Fix search when adding models to rotations

parent c4c3f2af
......@@ -4848,6 +4848,135 @@ async def get_provider_configured_models(request: Request, provider_id: str, sea
return JSONResponse({"models": model_names[:50]})
@app.get("/dashboard/providers/{provider_id}/search-models")
async def search_provider_models_api(request: Request, provider_id: str, query: str = "", refresh: bool = False):
"""Search provider models; fetches from live API if local config has none or refresh=True."""
auth_check = require_dashboard_auth(request)
if auth_check:
return JSONResponse({"models": [], "error": "unauthorized"}, status_code=401)
current_user_id = request.session.get('user_id')
is_config_admin = current_user_id is None
models = []
if is_config_admin:
try:
config_path = Path.home() / '.aisbf' / 'providers.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'providers.json'
with open(config_path) as f:
full_config = json.load(f)
if 'providers' in full_config and isinstance(full_config['providers'], dict):
providers_data = full_config['providers']
else:
providers_data = {k: v for k, v in full_config.items() if k != 'condensation'}
provider = providers_data.get(provider_id, {})
raw = provider.get('models', [])
models = [m.get('name', '') if isinstance(m, dict) else str(m) for m in raw]
models = [n for n in models if n]
except Exception:
pass
else:
try:
db = DatabaseRegistry.get_config_database()
user_providers = db.get_user_providers(current_user_id)
match = next((p for p in user_providers if p['provider_id'] == provider_id), None)
if match:
prov = match.get('config', match)
raw = prov.get('models', [])
models = [m.get('name', '') if isinstance(m, dict) else str(m) for m in raw]
models = [n for n in models if n]
except Exception:
pass
fetched_live = False
if not models or refresh:
try:
live = await fetch_provider_models(provider_id, user_id=current_user_id)
if live:
models = [m.get('name', m.get('id', '')) if isinstance(m, dict) else str(m) for m in live]
models = [n for n in models if n]
fetched_live = True
except Exception:
pass
if query:
q = query.lower()
models = [m for m in models if q in m.lower()]
return JSONResponse({"models": models[:200], "fetched_live": fetched_live})
@app.get("/dashboard/search-all-models")
async def search_all_models_api(request: Request, query: str = "", refresh: bool = False):
"""Return all available models (rotations + provider models) for autoselect, with optional live refresh."""
auth_check = require_dashboard_auth(request)
if auth_check:
return JSONResponse({"models": [], "error": "unauthorized"}, status_code=401)
current_user_id = request.session.get('user_id')
is_config_admin = current_user_id is None
all_models = []
if is_config_admin:
if config:
for rid in config.rotations:
all_models.append({'id': rid, 'name': f'{rid} (rotation)', 'type': 'rotation'})
try:
providers_path = Path.home() / '.aisbf' / 'providers.json'
if not providers_path.exists():
providers_path = Path(__file__).parent / 'config' / 'providers.json'
with open(providers_path) as f:
pc = json.load(f)
pd_map = pc.get('providers', {k: v for k, v in pc.items() if k != 'condensation'})
for pid, prov in pd_map.items():
pmodels = prov.get('models', [])
if not pmodels and refresh:
try:
live = await fetch_provider_models(pid)
if live:
pmodels = live
except Exception:
pass
for m in pmodels:
mname = m.get('name', m.get('id', '')) if isinstance(m, dict) else str(m)
if mname:
mid = f"{pid}/{mname}"
all_models.append({'id': mid, 'name': f"{mid} (provider model)", 'type': 'provider'})
except Exception:
pass
else:
try:
db = DatabaseRegistry.get_config_database()
for rot in db.get_user_rotations(current_user_id):
rid = rot['rotation_id']
all_models.append({'id': rid, 'name': f'{rid} (rotation)', 'type': 'rotation'})
for prov in db.get_user_providers(current_user_id):
pid = prov['provider_id']
pconfig = prov.get('config', prov)
pmodels = pconfig.get('models', [])
if not pmodels and refresh:
try:
live = await fetch_provider_models(pid, user_id=current_user_id)
if live:
pmodels = live
except Exception:
pass
for m in pmodels:
mname = m.get('name', m.get('id', '')) if isinstance(m, dict) else str(m)
if mname:
mid = f"{pid}/{mname}"
all_models.append({'id': mid, 'name': f"{mid} (provider model)", 'type': 'provider'})
except Exception:
pass
if query:
q = query.lower()
all_models = [m for m in all_models if q in m['id'].lower() or q in m['name'].lower()]
return JSONResponse({"models": all_models[:300]})
@app.get("/dashboard/rotations", response_class=HTMLResponse)
async def dashboard_rotations(request: Request):
"""Edit rotations configuration"""
......
......@@ -56,6 +56,103 @@ let availableRotations = autoselectData.rotations;
let availableModels = autoselectData.models;
let expandedAutoselects = new Set();
// ===== Model Search Modal =====
let _msAllItems = [], _msDispItems = [], _msCancelled = false, _msCallback = null;
function _msEnsure() {
if (document.getElementById('ms-overlay')) return;
const d = document.createElement('div');
d.id = 'ms-overlay';
d.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);z-index:9999;align-items:center;justify-content:center;';
d.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #0f3460;border-radius:8px;padding:24px;max-width:560px;width:93%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.6);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 id="ms-title" style="margin:0;font-size:16px;color:#e0e0e0;"></h3>
<button type="button" onclick="msClose()" style="background:none;border:none;color:#a0a0a0;cursor:pointer;font-size:22px;padding:0 4px;">&times;</button>
</div>
<div id="ms-status" style="font-size:13px;color:#a0a0a0;min-height:18px;margin-bottom:10px;"></div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<input type="text" id="ms-filter-input" placeholder="Filter results…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#16213e;color:#e0e0e0;font-size:14px;"
onkeydown="if(event.key==='Enter')msFilter()">
<button type="button" onclick="msFilter()" class="btn btn-secondary" style="padding:8px 14px;font-size:13px;">Filter</button>
</div>
<div id="ms-results" style="overflow-y:auto;flex:1;max-height:340px;border:1px solid #0f3460;border-radius:3px;background:#16213e;min-height:40px;"></div>
<div style="margin-top:14px;display:flex;justify-content:flex-end;">
<button type="button" onclick="msClose()" class="btn btn-secondary">Cancel</button>
</div>
</div>`;
document.body.appendChild(d);
}
async function msOpenAll(inputId, cb) {
_msEnsure(); _msCancelled = false; _msAllItems = []; _msDispItems = [];
_msCallback = function(v) { const e = document.getElementById(inputId); if (e) e.value = v; cb(v); };
const o = document.getElementById('ms-overlay'); o.style.display = 'flex';
document.getElementById('ms-title').textContent = 'Search Models';
document.getElementById('ms-filter-input').value = '';
const all = availableRotations.map(r => ({id: r, name: r + ' (rotation)'})).concat(availableModels);
if (all.length) {
_msAllItems = all;
document.getElementById('ms-status').textContent = all.length + ' model(s) available. Use Filter to narrow results.';
msRenderResults(all);
} else {
document.getElementById('ms-status').textContent = 'No models loaded — fetching from provider APIs…';
document.getElementById('ms-results').innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">Loading…</p>';
try {
const r = await fetch('/dashboard/search-all-models?refresh=true');
if (_msCancelled) return;
const data = await r.json(); const fetched = data.models || [];
_msAllItems = fetched;
availableModels = fetched.filter(m => m.type === 'provider');
document.getElementById('ms-status').textContent = fetched.length ? fetched.length + ' model(s) found.' : 'No models found.';
msRenderResults(fetched);
} catch(e) {
if (!_msCancelled) { document.getElementById('ms-status').textContent = 'Error: ' + e.message; document.getElementById('ms-results').innerHTML = '<p style="color:#f87171;padding:15px;">Failed to load.</p>'; }
}
}
}
function msFilter() {
const q = document.getElementById('ms-filter-input').value.trim().toLowerCase();
document.getElementById('ms-status').textContent = 'Searching…';
const filtered = q ? _msAllItems.filter(m => (typeof m === 'string' ? m : m.id + ' ' + m.name).toLowerCase().includes(q)) : _msAllItems;
document.getElementById('ms-status').textContent = filtered.length + ' result(s).';
msRenderResults(filtered);
}
function msRenderResults(items) {
_msDispItems = items;
const el = document.getElementById('ms-results');
if (!items || !items.length) { el.innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">No results.</p>'; return; }
el.innerHTML = items.map((item, i) => {
const isStr = typeof item === 'string';
const id = isStr ? item : item.id, label = isStr ? item : item.name;
return `<div data-idx="${i}" onclick="msPickIdx(this)" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #0a2a50;font-size:13px;word-break:break-all;" onmouseover="this.style.background='#0f3460'" onmouseout="this.style.background=''">` +
(id !== label ? `<span style="color:#c8d6e5;">${escHtmlAttr(id)}</span><span style="color:#888;font-size:11px;margin-left:6px;">${escHtmlAttr(label)}</span>` : `<span>${escHtmlAttr(id)}</span>`) + '</div>';
}).join('');
}
function msPickIdx(el) {
const i = parseInt(el.getAttribute('data-idx'));
const item = _msDispItems[i]; const val = typeof item === 'string' ? item : item.id;
if (_msCallback) _msCallback(val);
msClose();
}
function msClose() {
_msCancelled = true;
const el = document.getElementById('ms-overlay'); if (el) el.style.display = 'none';
}
function msOpenAllForInput(btn) {
const inputId = btn.getAttribute('data-input-id');
const input = document.getElementById(inputId);
msOpenAll(inputId, function(v) {
if (input) { input.value = v; input.dispatchEvent(new Event('change')); }
});
}
function buildModelSelectHtml(uid, currentValue, allModels, onChangeFn) {
if (allModels.length <= 25) {
const opts = allModels.map(m =>
......@@ -64,23 +161,19 @@ function buildModelSelectHtml(uid, currentValue, allModels, onChangeFn) {
return `<select id="${uid}" onchange="${onChangeFn}(this.value)" required>
<option value="">Select model...</option>${opts}</select>`;
} else {
const dlOpts = allModels.map(m => `<option value="${escHtmlAttr(m.id)}" label="${escHtmlAttr(m.name)}">`).join('');
return `<div style="position:relative;">
<input type="text" id="${uid}" value="${escHtmlAttr(currentValue)}" list="${uid}-dl"
placeholder="Type to search model..."
oninput="handleModelInput('${uid}',this.value,${onChangeFn})"
onchange="handleModelInput('${uid}',this.value,${onChangeFn})"
style="width:100%;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#1a1a2e;color:#e0e0e0;font-size:14px;">
<datalist id="${uid}-dl">${dlOpts}</datalist>
return `<div style="display:flex;gap:5px;align-items:center;">
<input type="text" id="${escHtmlAttr(uid)}" value="${escHtmlAttr(currentValue)}"
onchange="${onChangeFn}(this.value)"
placeholder="Model ID…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#1a1a2e;color:#e0e0e0;font-size:14px;">
<button type="button" class="btn btn-secondary"
data-input-id="${escHtmlAttr(uid)}"
onclick="msOpenAllForInput(this)"
style="padding:7px 12px;white-space:nowrap;">Search</button>
</div>`;
}
}
function handleModelInput(uid, value, callback) {
const allIds = availableModels.map(m => m.id).concat(availableRotations).concat(['internal']);
if (allIds.includes(value) || value === '') callback(value);
}
function renderAutoselectList() {
const container = document.getElementById('autoselect-list');
container.innerHTML = '';
......
......@@ -61,32 +61,101 @@ let rotationsConfig = rotationsData.config;
let availableProviders = rotationsData.providers;
let expandedRotations = new Set();
// Model name autocomplete: cache per provider
const providerModelsCache = {};
const modelSearchTimers = {};
async function fetchProviderModels(providerId) {
if (!providerId) return [];
if (providerModelsCache[providerId]) return providerModelsCache[providerId];
// ===== Model Search Modal =====
let _msAllItems = [], _msDispItems = [], _msCancelled = false, _msCallback = null;
function _msEnsure() {
if (document.getElementById('ms-overlay')) return;
const d = document.createElement('div');
d.id = 'ms-overlay';
d.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);z-index:9999;align-items:center;justify-content:center;';
d.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #0f3460;border-radius:8px;padding:24px;max-width:560px;width:93%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.6);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 id="ms-title" style="margin:0;font-size:16px;color:#e0e0e0;"></h3>
<button type="button" onclick="msClose()" style="background:none;border:none;color:#a0a0a0;cursor:pointer;font-size:22px;padding:0 4px;">&times;</button>
</div>
<div id="ms-status" style="font-size:13px;color:#a0a0a0;min-height:18px;margin-bottom:10px;"></div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<input type="text" id="ms-filter-input" placeholder="Filter results…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#16213e;color:#e0e0e0;font-size:14px;"
onkeydown="if(event.key==='Enter')msFilter()">
<button type="button" onclick="msFilter()" class="btn btn-secondary" style="padding:8px 14px;font-size:13px;">Filter</button>
</div>
<div id="ms-results" style="overflow-y:auto;flex:1;max-height:340px;border:1px solid #0f3460;border-radius:3px;background:#16213e;min-height:40px;"></div>
<div style="margin-top:14px;display:flex;justify-content:flex-end;">
<button type="button" onclick="msClose()" class="btn btn-secondary">Cancel</button>
</div>
</div>`;
document.body.appendChild(d);
}
async function msOpenProvider(providerId, inputId, cb) {
_msEnsure(); _msCancelled = false; _msAllItems = []; _msDispItems = [];
_msCallback = function(v) { const e = document.getElementById(inputId); if (e) e.value = v; cb(v); };
const o = document.getElementById('ms-overlay'); o.style.display = 'flex';
document.getElementById('ms-title').textContent = 'Search Models — ' + providerId;
document.getElementById('ms-filter-input').value = '';
document.getElementById('ms-status').textContent = 'Checking provider models…';
document.getElementById('ms-results').innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">Loading…</p>';
try {
const resp = await fetch(`/dashboard/providers/${encodeURIComponent(providerId)}/configured-models`);
if (!resp.ok) return [];
const data = await resp.json();
providerModelsCache[providerId] = data.models || [];
return providerModelsCache[providerId];
} catch { return []; }
}
function searchModelsDebounced(datalistId, query, providerId, timerKey) {
clearTimeout(modelSearchTimers[timerKey]);
if (query.length < 3 || !providerId) return;
modelSearchTimers[timerKey] = setTimeout(async () => {
let models = await fetchProviderModels(providerId);
const q = query.toLowerCase();
models = models.filter(m => m.toLowerCase().includes(q)).slice(0, 30);
const dl = document.getElementById(datalistId);
if (dl) dl.innerHTML = models.map(m => `<option value="${escHtmlAttr(m)}">`).join('');
}, 350);
let r = await fetch('/dashboard/providers/' + encodeURIComponent(providerId) + '/configured-models');
if (_msCancelled) return;
let d = await r.json(); let m = d.models || [];
if (!m.length) {
document.getElementById('ms-status').textContent = 'No local model list — fetching from provider API…';
r = await fetch('/dashboard/providers/' + encodeURIComponent(providerId) + '/search-models?refresh=true');
if (_msCancelled) return;
d = await r.json(); m = d.models || [];
}
if (_msCancelled) return;
_msAllItems = m;
document.getElementById('ms-status').textContent = m.length ? m.length + ' model(s) found. Use Filter to narrow results.' : 'No models found for this provider.';
msRenderResults(m);
} catch(e) {
if (!_msCancelled) { document.getElementById('ms-status').textContent = 'Error: ' + e.message; document.getElementById('ms-results').innerHTML = '<p style="color:#f87171;padding:15px;">Failed to load models.</p>'; }
}
}
function msFilter() {
const q = document.getElementById('ms-filter-input').value.trim().toLowerCase();
document.getElementById('ms-status').textContent = 'Searching…';
const filtered = q ? _msAllItems.filter(m => (typeof m === 'string' ? m : m.id + ' ' + m.name).toLowerCase().includes(q)) : _msAllItems;
document.getElementById('ms-status').textContent = filtered.length + ' result(s).';
msRenderResults(filtered);
}
function msRenderResults(items) {
_msDispItems = items;
const el = document.getElementById('ms-results');
if (!items || !items.length) { el.innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">No results.</p>'; return; }
el.innerHTML = items.map((item, i) => {
const isStr = typeof item === 'string';
const id = isStr ? item : item.id, label = isStr ? item : item.name;
return `<div data-idx="${i}" onclick="msPickIdx(this)" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #0a2a50;font-size:13px;word-break:break-all;" onmouseover="this.style.background='#0f3460'" onmouseout="this.style.background=''">` +
(id !== label ? `<span style="color:#c8d6e5;">${escHtmlAttr(id)}</span><span style="color:#888;font-size:11px;margin-left:6px;">${escHtmlAttr(label)}</span>` : `<span>${escHtmlAttr(id)}</span>`) + '</div>';
}).join('');
}
function msPickIdx(el) {
const i = parseInt(el.getAttribute('data-idx'));
const item = _msDispItems[i]; const val = typeof item === 'string' ? item : item.id;
if (_msCallback) _msCallback(val);
msClose();
}
function msClose() {
_msCancelled = true;
const el = document.getElementById('ms-overlay'); if (el) el.style.display = 'none';
}
function msOpenProviderForInput(btn) {
const providerId = btn.getAttribute('data-provider');
const inputId = btn.getAttribute('data-input-id');
const input = document.getElementById(inputId);
msOpenProvider(providerId, inputId, function(v) {
if (input) { input.value = v; input.dispatchEvent(new Event('change')); }
});
}
// Provider select widget: <select> for 25, datalist input for >25
......@@ -282,8 +351,7 @@ function renderRotationModels(rotationKey, providerIndex) {
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid #0f3460; padding: 10px; margin-bottom: 8px; border-radius: 3px; background: #16213e;';
const safeKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const dlId = `mdl-dl-${rotationKey}-${providerIndex}-${modelIndex}`;
const timerKey = `${rotationKey}-${providerIndex}-${modelIndex}`;
const inputId = `mdl-inp-${rotationKey}-${providerIndex}-${modelIndex}`;
const providerId = provider.provider_id || '';
modelDiv.innerHTML = `
......@@ -294,12 +362,18 @@ function renderRotationModels(rotationKey, providerIndex) {
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Model Name</label>
<input type="text" value="${escHtmlAttr(model.name)}" list="${dlId}"
placeholder="Type 3+ chars to search..."
oninput="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value);searchModelsDebounced('${dlId}',this.value,'${escHtmlAttr(providerId)}','${timerKey}')"
onchange="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
required style="font-size: 12px; padding: 5px;">
<datalist id="${dlId}"></datalist>
<div style="display:flex;gap:5px;align-items:center;">
<input type="text" id="${escHtmlAttr(inputId)}" value="${escHtmlAttr(model.name)}"
oninput="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
onchange="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
placeholder="Model name…"
required style="flex:1;font-size:12px;padding:5px;">
<button type="button" class="btn btn-secondary"
data-provider="${escHtmlAttr(providerId)}"
data-input-id="${escHtmlAttr(inputId)}"
onclick="msOpenProviderForInput(this)"
style="padding:4px 8px;font-size:11px;white-space:nowrap;">Search</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 8px;">
......
......@@ -52,7 +52,103 @@ async function apiCall(method, url, body) {
return r.json();
}
// Build a searchable select for model lists (datalist when >25 options)
// ===== Model Search Modal =====
let _msAllItems = [], _msDispItems = [], _msCancelled = false, _msCallback = null;
function _msEnsure() {
if (document.getElementById('ms-overlay')) return;
const d = document.createElement('div');
d.id = 'ms-overlay';
d.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);z-index:9999;align-items:center;justify-content:center;';
d.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #0f3460;border-radius:8px;padding:24px;max-width:560px;width:93%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.6);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 id="ms-title" style="margin:0;font-size:16px;color:#e0e0e0;"></h3>
<button type="button" onclick="msClose()" style="background:none;border:none;color:#a0a0a0;cursor:pointer;font-size:22px;padding:0 4px;">&times;</button>
</div>
<div id="ms-status" style="font-size:13px;color:#a0a0a0;min-height:18px;margin-bottom:10px;"></div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<input type="text" id="ms-filter-input" placeholder="Filter results…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#16213e;color:#e0e0e0;font-size:14px;"
onkeydown="if(event.key==='Enter')msFilter()">
<button type="button" onclick="msFilter()" class="btn btn-secondary" style="padding:8px 14px;font-size:13px;">Filter</button>
</div>
<div id="ms-results" style="overflow-y:auto;flex:1;max-height:340px;border:1px solid #0f3460;border-radius:3px;background:#16213e;min-height:40px;"></div>
<div style="margin-top:14px;display:flex;justify-content:flex-end;">
<button type="button" onclick="msClose()" class="btn btn-secondary">Cancel</button>
</div>
</div>`;
document.body.appendChild(d);
}
async function msOpenAll(inputId, cb) {
_msEnsure(); _msCancelled = false; _msAllItems = []; _msDispItems = [];
_msCallback = function(v) { const e = document.getElementById(inputId); if (e) e.value = v; cb(v); };
const o = document.getElementById('ms-overlay'); o.style.display = 'flex';
document.getElementById('ms-title').textContent = 'Search Models';
document.getElementById('ms-filter-input').value = '';
const all = availableRotations.map(r => ({id: r, name: r + ' (rotation)'})).concat(availableModels);
if (all.length) {
_msAllItems = all;
document.getElementById('ms-status').textContent = all.length + ' model(s) available. Use Filter to narrow results.';
msRenderResults(all);
} else {
document.getElementById('ms-status').textContent = 'No models loaded — fetching from provider APIs…';
document.getElementById('ms-results').innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">Loading…</p>';
try {
const r = await fetch('/dashboard/search-all-models?refresh=true');
if (_msCancelled) return;
const data = await r.json(); const fetched = data.models || [];
_msAllItems = fetched;
availableModels = fetched.filter(m => m.type === 'provider');
document.getElementById('ms-status').textContent = fetched.length ? fetched.length + ' model(s) found.' : 'No models found.';
msRenderResults(fetched);
} catch(e) {
if (!_msCancelled) { document.getElementById('ms-status').textContent = 'Error: ' + e.message; document.getElementById('ms-results').innerHTML = '<p style="color:#f87171;padding:15px;">Failed to load.</p>'; }
}
}
}
function msFilter() {
const q = document.getElementById('ms-filter-input').value.trim().toLowerCase();
document.getElementById('ms-status').textContent = 'Searching…';
const filtered = q ? _msAllItems.filter(m => (typeof m === 'string' ? m : m.id + ' ' + m.name).toLowerCase().includes(q)) : _msAllItems;
document.getElementById('ms-status').textContent = filtered.length + ' result(s).';
msRenderResults(filtered);
}
function msRenderResults(items) {
_msDispItems = items;
const el = document.getElementById('ms-results');
if (!items || !items.length) { el.innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">No results.</p>'; return; }
el.innerHTML = items.map((item, i) => {
const isStr = typeof item === 'string';
const id = isStr ? item : item.id, label = isStr ? item : item.name;
return `<div data-idx="${i}" onclick="msPickIdx(this)" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #0a2a50;font-size:13px;word-break:break-all;" onmouseover="this.style.background='#0f3460'" onmouseout="this.style.background=''">` +
(id !== label ? `<span style="color:#c8d6e5;">${escHtml(id)}</span><span style="color:#888;font-size:11px;margin-left:6px;">${escHtml(label)}</span>` : `<span>${escHtml(id)}</span>`) + '</div>';
}).join('');
}
function msPickIdx(el) {
const i = parseInt(el.getAttribute('data-idx'));
const item = _msDispItems[i]; const val = typeof item === 'string' ? item : item.id;
if (_msCallback) _msCallback(val);
msClose();
}
function msClose() {
_msCancelled = true;
const el = document.getElementById('ms-overlay'); if (el) el.style.display = 'none';
}
function msOpenAllForInput(btn) {
const inputId = btn.getAttribute('data-input-id');
const input = document.getElementById(inputId);
msOpenAll(inputId, function(v) {
if (input) { input.value = v; input.dispatchEvent(new Event('change')); }
});
}
function buildModelSelectHtml(uid, currentValue, allModels, onChangeFn) {
if (allModels.length <= 25) {
const opts = allModels.map(m =>
......@@ -61,26 +157,19 @@ function buildModelSelectHtml(uid, currentValue, allModels, onChangeFn) {
return `<select id="${uid}" onchange="${onChangeFn}(this.value)" required>
<option value="">Select model...</option>${opts}</select>`;
} else {
const currentLabel = (allModels.find(m => m.id === currentValue) || {}).name || currentValue;
const dlOpts = allModels.map(m => `<option value="${escHtml(m.id)}" label="${escHtml(m.name)}">`).join('');
return `<div style="position:relative;">
<input type="text" id="${uid}" value="${escHtml(currentValue)}" list="${uid}-dl"
placeholder="Type to search model..."
oninput="handleModelInput('${uid}',this.value,${onChangeFn})"
onchange="handleModelInput('${uid}',this.value,${onChangeFn})"
style="width:100%;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#1a1a2e;color:#e0e0e0;font-size:14px;">
<datalist id="${uid}-dl">${dlOpts}</datalist>
return `<div style="display:flex;gap:5px;align-items:center;">
<input type="text" id="${escHtml(uid)}" value="${escHtml(currentValue)}"
onchange="${onChangeFn}(this.value)"
placeholder="Model ID…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#1a1a2e;color:#e0e0e0;font-size:14px;">
<button type="button" class="btn btn-secondary"
data-input-id="${escHtml(uid)}"
onclick="msOpenAllForInput(this)"
style="padding:7px 12px;white-space:nowrap;">Search</button>
</div>`;
}
}
function handleModelInput(uid, value, callback) {
const allIds = availableModels.map(m => m.id).concat(availableRotations);
if (allIds.includes(value) || value === 'internal' || value === '') {
callback(value);
}
}
const autoselectData = {
config: {{ autoselect_json | safe }},
rotations: {{ available_rotations | safe }},
......
......@@ -54,31 +54,101 @@ let rotationsConfig = rotationsData.config;
let availableProviders = rotationsData.providers;
let expandedRotations = new Set();
const providerModelsCache = {};
const modelSearchTimers = {};
async function fetchProviderModels(providerId) {
if (!providerId) return [];
if (providerModelsCache[providerId]) return providerModelsCache[providerId];
// ===== Model Search Modal =====
let _msAllItems = [], _msDispItems = [], _msCancelled = false, _msCallback = null;
function _msEnsure() {
if (document.getElementById('ms-overlay')) return;
const d = document.createElement('div');
d.id = 'ms-overlay';
d.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);z-index:9999;align-items:center;justify-content:center;';
d.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #0f3460;border-radius:8px;padding:24px;max-width:560px;width:93%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.6);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 id="ms-title" style="margin:0;font-size:16px;color:#e0e0e0;"></h3>
<button type="button" onclick="msClose()" style="background:none;border:none;color:#a0a0a0;cursor:pointer;font-size:22px;padding:0 4px;">&times;</button>
</div>
<div id="ms-status" style="font-size:13px;color:#a0a0a0;min-height:18px;margin-bottom:10px;"></div>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<input type="text" id="ms-filter-input" placeholder="Filter results…"
style="flex:1;padding:8px;border:1px solid #0f3460;border-radius:3px;background:#16213e;color:#e0e0e0;font-size:14px;"
onkeydown="if(event.key==='Enter')msFilter()">
<button type="button" onclick="msFilter()" class="btn btn-secondary" style="padding:8px 14px;font-size:13px;">Filter</button>
</div>
<div id="ms-results" style="overflow-y:auto;flex:1;max-height:340px;border:1px solid #0f3460;border-radius:3px;background:#16213e;min-height:40px;"></div>
<div style="margin-top:14px;display:flex;justify-content:flex-end;">
<button type="button" onclick="msClose()" class="btn btn-secondary">Cancel</button>
</div>
</div>`;
document.body.appendChild(d);
}
async function msOpenProvider(providerId, inputId, cb) {
_msEnsure(); _msCancelled = false; _msAllItems = []; _msDispItems = [];
_msCallback = function(v) { const e = document.getElementById(inputId); if (e) e.value = v; cb(v); };
const o = document.getElementById('ms-overlay'); o.style.display = 'flex';
document.getElementById('ms-title').textContent = 'Search Models — ' + providerId;
document.getElementById('ms-filter-input').value = '';
document.getElementById('ms-status').textContent = 'Checking provider models…';
document.getElementById('ms-results').innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">Loading…</p>';
try {
const resp = await fetch(`/dashboard/providers/${encodeURIComponent(providerId)}/configured-models`);
if (!resp.ok) return [];
const data = await resp.json();
providerModelsCache[providerId] = data.models || [];
return providerModelsCache[providerId];
} catch { return []; }
}
function searchModelsDebounced(datalistId, query, providerId, timerKey) {
clearTimeout(modelSearchTimers[timerKey]);
if (query.length < 3 || !providerId) return;
modelSearchTimers[timerKey] = setTimeout(async () => {
let models = await fetchProviderModels(providerId);
const q = query.toLowerCase();
models = models.filter(m => m.toLowerCase().includes(q)).slice(0, 30);
const dl = document.getElementById(datalistId);
if (dl) dl.innerHTML = models.map(m => `<option value="${escHtmlAttr(m)}">`).join('');
}, 350);
let r = await fetch('/dashboard/providers/' + encodeURIComponent(providerId) + '/configured-models');
if (_msCancelled) return;
let d = await r.json(); let m = d.models || [];
if (!m.length) {
document.getElementById('ms-status').textContent = 'No local model list — fetching from provider API…';
r = await fetch('/dashboard/providers/' + encodeURIComponent(providerId) + '/search-models?refresh=true');
if (_msCancelled) return;
d = await r.json(); m = d.models || [];
}
if (_msCancelled) return;
_msAllItems = m;
document.getElementById('ms-status').textContent = m.length ? m.length + ' model(s) found. Use Filter to narrow results.' : 'No models found for this provider.';
msRenderResults(m);
} catch(e) {
if (!_msCancelled) { document.getElementById('ms-status').textContent = 'Error: ' + e.message; document.getElementById('ms-results').innerHTML = '<p style="color:#f87171;padding:15px;">Failed to load models.</p>'; }
}
}
function msFilter() {
const q = document.getElementById('ms-filter-input').value.trim().toLowerCase();
document.getElementById('ms-status').textContent = 'Searching…';
const filtered = q ? _msAllItems.filter(m => (typeof m === 'string' ? m : m.id + ' ' + m.name).toLowerCase().includes(q)) : _msAllItems;
document.getElementById('ms-status').textContent = filtered.length + ' result(s).';
msRenderResults(filtered);
}
function msRenderResults(items) {
_msDispItems = items;
const el = document.getElementById('ms-results');
if (!items || !items.length) { el.innerHTML = '<p style="color:#a0a0a0;text-align:center;padding:20px;">No results.</p>'; return; }
el.innerHTML = items.map((item, i) => {
const isStr = typeof item === 'string';
const id = isStr ? item : item.id, label = isStr ? item : item.name;
return `<div data-idx="${i}" onclick="msPickIdx(this)" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #0a2a50;font-size:13px;word-break:break-all;" onmouseover="this.style.background='#0f3460'" onmouseout="this.style.background=''">` +
(id !== label ? `<span style="color:#c8d6e5;">${escHtmlAttr(id)}</span><span style="color:#888;font-size:11px;margin-left:6px;">${escHtmlAttr(label)}</span>` : `<span>${escHtmlAttr(id)}</span>`) + '</div>';
}).join('');
}
function msPickIdx(el) {
const i = parseInt(el.getAttribute('data-idx'));
const item = _msDispItems[i]; const val = typeof item === 'string' ? item : item.id;
if (_msCallback) _msCallback(val);
msClose();
}
function msClose() {
_msCancelled = true;
const el = document.getElementById('ms-overlay'); if (el) el.style.display = 'none';
}
function msOpenProviderForInput(btn) {
const providerId = btn.getAttribute('data-provider');
const inputId = btn.getAttribute('data-input-id');
const input = document.getElementById(inputId);
msOpenProvider(providerId, inputId, function(v) {
if (input) { input.value = v; input.dispatchEvent(new Event('change')); }
});
}
function buildProviderSelectHtml(uid, currentValue, onChangeFn) {
......@@ -275,8 +345,7 @@ function renderRotationModels(rotationKey, providerIndex) {
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid #0f3460; padding: 10px; margin-bottom: 8px; border-radius: 3px; background: #16213e;';
const safeKey = rotationKey.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const dlId = `mdl-dl-${rotationKey}-${providerIndex}-${modelIndex}`;
const timerKey = `${rotationKey}-${providerIndex}-${modelIndex}`;
const inputId = `mdl-inp-${rotationKey}-${providerIndex}-${modelIndex}`;
const providerId = provider.provider_id || '';
modelDiv.innerHTML = `
......@@ -287,12 +356,18 @@ function renderRotationModels(rotationKey, providerIndex) {
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Model Name</label>
<input type="text" value="${escHtmlAttr(model.name)}" list="${dlId}"
placeholder="Type 3+ chars to search..."
oninput="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value);searchModelsDebounced('${dlId}',this.value,'${escHtmlAttr(providerId)}','${timerKey}')"
onchange="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
required style="font-size: 12px; padding: 5px;">
<datalist id="${dlId}"></datalist>
<div style="display:flex;gap:5px;align-items:center;">
<input type="text" id="${escHtmlAttr(inputId)}" value="${escHtmlAttr(model.name)}"
oninput="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
onchange="updateRotationModel('${safeKey}',${providerIndex},${modelIndex},'name',this.value)"
placeholder="Model name…"
required style="flex:1;font-size:12px;padding:5px;">
<button type="button" class="btn btn-secondary"
data-provider="${escHtmlAttr(providerId)}"
data-input-id="${escHtmlAttr(inputId)}"
onclick="msOpenProviderForInput(this)"
style="padding:4px 8px;font-size:11px;white-space:nowrap;">Search</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 8px;">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment