Still coderai

parent 32b55111
...@@ -601,7 +601,7 @@ async def dashboard_studio(request: Request): ...@@ -601,7 +601,7 @@ async def dashboard_studio(request: Request):
is_config_admin = request.session.get("role") == "admin" and current_user_id is None is_config_admin = request.session.get("role") == "admin" and current_user_id is None
scope = "admin" if is_config_admin else "user" scope = "admin" if is_config_admin else "user"
db = None if is_config_admin else DatabaseRegistry.get_config_database() db = None if is_config_admin else DatabaseRegistry.get_config_database()
catalog = build_studio_catalog( catalog = await build_studio_catalog(
scope=scope, scope=scope,
owner_id=current_user_id, owner_id=current_user_id,
config=_config, config=_config,
...@@ -636,7 +636,7 @@ async def dashboard_studio_catalog(request: Request): ...@@ -636,7 +636,7 @@ async def dashboard_studio_catalog(request: Request):
scope = "admin" if request.session.get("role") == "admin" else "user" scope = "admin" if request.session.get("role") == "admin" else "user"
db = None if scope == "admin" else DatabaseRegistry.get_config_database() db = None if scope == "admin" else DatabaseRegistry.get_config_database()
catalog = build_studio_catalog( catalog = await build_studio_catalog(
scope=scope, scope=scope,
owner_id=current_user_id, owner_id=current_user_id,
config=_config, config=_config,
......
...@@ -19,7 +19,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -19,7 +19,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import json import json
...@@ -576,7 +575,7 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects: ...@@ -576,7 +575,7 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects:
return entries return entries
def build_studio_catalog( async def build_studio_catalog(
*, *,
scope: str, scope: str,
owner_id: Optional[int], owner_id: Optional[int],
...@@ -608,9 +607,7 @@ def build_studio_catalog( ...@@ -608,9 +607,7 @@ def build_studio_catalog(
if provider_config is None: if provider_config is None:
continue continue
try: try:
live_models = asyncio.run(get_provider_models(provider_id, provider_config, config, user_id=owner_id if scope == "user" else None)) live_models = await get_provider_models(provider_id, provider_config, config, user_id=owner_id if scope == "user" else None)
except RuntimeError:
live_models = []
except Exception: except Exception:
live_models = [] live_models = []
if not live_models: if not live_models:
......
...@@ -112,6 +112,13 @@ ...@@ -112,6 +112,13 @@
.binding-role-result-meta { font-size:10px; color:var(--text-3); margin-top:.08rem; } .binding-role-result-meta { font-size:10px; color:var(--text-3); margin-top:.08rem; }
.binding-role-clear { align-self:flex-start; } .binding-role-clear { align-self:flex-start; }
.binding-empty { font-size:11px; color:var(--text-3); padding:.3rem 0; } .binding-empty { font-size:11px; color:var(--text-3); padding:.3rem 0; }
.binding-provider-chips { display:flex; flex-wrap:wrap; gap:.3rem; padding-bottom:.15rem; }
.binding-provider-chip { display:inline-flex; align-items:center; gap:.3rem; padding:.2rem .45rem; border:1px solid var(--border); border-radius:999px; background:var(--surface-2); color:var(--text-2); font-size:10px; font-weight:600; font-family:inherit; cursor:pointer; white-space:nowrap; }
.binding-provider-chip:hover { background:var(--accent-dim); border-color:color-mix(in srgb, var(--accent) 50%, var(--border)); color:var(--text-1); }
.binding-chip-count { background:var(--surface-3); border-radius:999px; padding:.05rem .3rem; font-size:9px; font-weight:400; color:var(--text-3); }
.binding-provider-breadcrumb { display:flex; align-items:center; gap:.3rem; padding:.2rem 0 .1rem; }
.binding-provider-crumb-name { font-size:11px; font-weight:700; color:var(--accent,#4e9cf5); }
.binding-provider-crumb-hint { font-size:10px; color:var(--text-3); }
.model-item:hover { background:var(--surface-2); } .model-item:hover { background:var(--surface-2); }
.model-item.active { background:var(--accent-dim,#1d3354); color:var(--accent,#4e9cf5); font-weight:500; } .model-item.active { background:var(--accent-dim,#1d3354); color:var(--accent,#4e9cf5); font-weight:500; }
.mbadge { .mbadge {
......
...@@ -29,6 +29,7 @@ let functionBindingDefs = []; ...@@ -29,6 +29,7 @@ let functionBindingDefs = [];
let functionBindings = {}; let functionBindings = {};
let bindingSearchState = {}; let bindingSearchState = {};
let selectedBindingId = 'chat'; let selectedBindingId = 'chat';
let _pendingBindingFocusKey = null;
function _startVidPoll(prefix) { function _startVidPoll(prefix) {
if (_vidPollTimer) { clearInterval(_vidPollTimer); _vidPollTimer = null; } if (_vidPollTimer) { clearInterval(_vidPollTimer); _vidPollTimer = null; }
...@@ -1617,7 +1618,14 @@ function renderSidebar() { ...@@ -1617,7 +1618,14 @@ function renderSidebar() {
const restoreKey = activeIsBindingSearch ? activeEl.getAttribute('data-search-key') : null; const restoreKey = activeIsBindingSearch ? activeEl.getAttribute('data-search-key') : null;
if (!functionBindingDefs.length) { el.innerHTML='<div class="muted small" style="padding:.5rem .6rem">No Studio bindings</div>'; return; } if (!functionBindingDefs.length) { el.innerHTML='<div class="muted small" style="padding:.5rem .6rem">No Studio bindings</div>'; return; }
el.innerHTML = `<div class="binding-list">${functionBindingDefs.map(renderBindingCard).join('')}</div>`; el.innerHTML = `<div class="binding-list">${functionBindingDefs.map(renderBindingCard).join('')}</div>`;
if (restoreKey) { if (_pendingBindingFocusKey) {
const pendingEl = el.querySelector(`.binding-role-search[data-search-key="${CSS.escape(_pendingBindingFocusKey)}"]`);
if (pendingEl) {
pendingEl.focus();
pendingEl.setSelectionRange(pendingEl.value.length, pendingEl.value.length);
}
_pendingBindingFocusKey = null;
} else if (restoreKey) {
const nextEl = el.querySelector(`.binding-role-search[data-search-key="${CSS.escape(restoreKey)}"]`); const nextEl = el.querySelector(`.binding-role-search[data-search-key="${CSS.escape(restoreKey)}"]`);
if (nextEl) { if (nextEl) {
nextEl.focus(); nextEl.focus();
...@@ -1646,24 +1654,93 @@ function renderBindingCard(def) { ...@@ -1646,24 +1654,93 @@ function renderBindingCard(def) {
</div>`; </div>`;
} }
function _providerSuggestionsForRole(role, queryLower) {
const capable = modelsForRole(role);
const map = new Map();
for (const model of capable) {
if (model.studioKind !== 'provider_model') continue;
const pid = model.sourceId || model.id?.split('/')[1] || '';
if (!pid) continue;
map.set(pid, (map.get(pid) || 0) + 1);
}
let providers = [...map.entries()].map(([id, count]) => ({ id, count }));
if (queryLower) {
providers = providers.filter(p => p.id.toLowerCase().includes(queryLower));
providers.sort((a, b) => {
const aPrefix = a.id.toLowerCase().startsWith(queryLower) ? 0 : 1;
const bPrefix = b.id.toLowerCase().startsWith(queryLower) ? 0 : 1;
if (aPrefix !== bPrefix) return aPrefix - bPrefix;
return a.id.localeCompare(b.id);
});
} else {
providers.sort((a, b) => a.id.localeCompare(b.id));
}
return providers;
}
function _renderBindingModelResult(def, role, model, assignedId) {
const isActive = model.id === assignedId;
return `<button class="binding-role-result${isActive ? ' active' : ''}" onclick="saveBindingRole('${def.id}','${role.key}','${encodeURIComponent(model.id)}');return false;">
<span>
<div class="binding-role-result-name">${escapeHtml(displayNameForModel(model))}</div>
<div class="binding-role-result-meta">${escapeHtml(model.id)} · ${escapeHtml(model.studioKind || model.type || 'model')}</div>
</span>
<span class="binding-role-result-meta">${escapeHtml((model.capabilities || []).slice(0, 3).join(', ') || 'capability')}</span>
</button>`;
}
function setBindingSearch(bindingId, roleKey, value) {
const key = `${bindingId}:${roleKey}`;
bindingSearchState[key] = value;
_pendingBindingFocusKey = key;
renderSidebar();
}
function renderBindingRole(def, role) { function renderBindingRole(def, role) {
const searchKey = `${def.id}:${role.key}`; const searchKey = `${def.id}:${role.key}`;
const query = bindingSearchState[searchKey] || ''; const query = bindingSearchState[searchKey] || '';
const assignedId = bindingModelId(def.id, role.key); const assignedId = bindingModelId(def.id, role.key);
const assignedModel = models.find(model => model.id === assignedId) || null; const assignedModel = models.find(model => model.id === assignedId) || null;
const candidates = modelsForRole(role).filter(model => modelMatchesQuery(model, query));
const results = candidates.length let resultsHtml;
? candidates.slice(0, 50).map(model => { const slashIdx = query.indexOf('/');
const isActive = model.id === assignedId;
return `<button class="binding-role-result${isActive ? ' active' : ''}" onclick="saveBindingRole('${def.id}','${role.key}','${encodeURIComponent(model.id)}');return false;"> if (slashIdx !== -1) {
<span> // Provider-scoped: "providerid/" or "providerid/modelpart"
<div class="binding-role-result-name">${escapeHtml(displayNameForModel(model))}</div> const providerPart = query.slice(0, slashIdx).toLowerCase();
<div class="binding-role-result-meta">${escapeHtml(model.id)} · ${escapeHtml(model.studioKind || model.type || 'model')}</div> const modelPart = query.slice(slashIdx + 1).toLowerCase();
</span> const capable = modelsForRole(role);
<span class="binding-role-result-meta">${escapeHtml((model.capabilities || []).slice(0, 3).join(', ') || 'capability')}</span> const candidates = capable.filter(model => {
</button>`; const pid = (model.sourceId || model.id?.split('/')[1] || '').toLowerCase();
}).join('') if (pid !== providerPart) return false;
: `<div class="binding-empty">No matching provider/model, rotation, or autoselect with the required capability.</div>`; if (!modelPart) return true;
const tgt = (model.targetId || '').toLowerCase();
const name = displayNameForModel(model).toLowerCase();
return tgt.includes(modelPart) || name.includes(modelPart);
});
const breadcrumb = `<div class="binding-provider-breadcrumb"><span class="binding-provider-crumb-name">${escapeHtml(query.slice(0, slashIdx))}/</span><span class="binding-provider-crumb-hint">type to filter models</span></div>`;
resultsHtml = breadcrumb + (candidates.length
? candidates.slice(0, 50).map(model => _renderBindingModelResult(def, role, model, assignedId)).join('')
: `<div class="binding-empty">No matching model for "${escapeHtml(providerPart)}" with the required capability.</div>`);
} else {
const q = query.trim().toLowerCase();
const capable = modelsForRole(role);
const providers = _providerSuggestionsForRole(role, q);
const providerHtml = providers.length
? `<div class="binding-provider-chips">${providers.slice(0, 8).map(p =>
`<button class="binding-provider-chip" onclick="setBindingSearch('${escapeHtml(def.id)}','${escapeHtml(role.key)}','${escapeHtml(p.id)}/');return false;">${escapeHtml(p.id)}<span class="binding-chip-count">${p.count}</span></button>`
).join('')}</div>`
: '';
const modelCandidates = q ? capable.filter(model => modelMatchesQuery(model, q)) : capable;
const modelHtml = modelCandidates.length
? modelCandidates.slice(0, q ? 50 : 20).map(model => _renderBindingModelResult(def, role, model, assignedId)).join('')
: '';
const emptyHtml = !providerHtml && !modelHtml
? `<div class="binding-empty">${q ? 'No matching provider/model, rotation, or autoselect with the required capability.' : 'No models available with the required capability.'}</div>`
: '';
resultsHtml = providerHtml + modelHtml + emptyHtml;
}
const currentMeta = assignedModel const currentMeta = assignedModel
? `${escapeHtml(assignedModel.id)} · ${escapeHtml((role.capabilities || []).join(' or '))}` ? `${escapeHtml(assignedModel.id)} · ${escapeHtml((role.capabilities || []).join(' or '))}`
: `Needs ${escapeHtml((role.capabilities || []).join(' or '))}`; : `Needs ${escapeHtml((role.capabilities || []).join(' or '))}`;
...@@ -1674,7 +1751,7 @@ function renderBindingRole(def, role) { ...@@ -1674,7 +1751,7 @@ function renderBindingRole(def, role) {
</div> </div>
<div class="binding-role-meta">${currentMeta}</div> <div class="binding-role-meta">${currentMeta}</div>
<input class="fi binding-role-search" type="search" data-search-key="${escapeHtml(searchKey)}" value="${escapeHtml(query)}" placeholder="Search provider/model, rotation, autoselect" oninput="updateBindingSearch('${def.id}','${role.key}', this.value)"> <input class="fi binding-role-search" type="search" data-search-key="${escapeHtml(searchKey)}" value="${escapeHtml(query)}" placeholder="Search provider/model, rotation, autoselect" oninput="updateBindingSearch('${def.id}','${role.key}', this.value)">
<div class="binding-role-results">${results}</div> <div class="binding-role-results">${resultsHtml}</div>
${assignedModel ? `<button class="btn btn-ghost btn-sm binding-role-clear" onclick="clearBindingRole('${def.id}','${role.key}');return false;">Clear</button>` : ''} ${assignedModel ? `<button class="btn btn-ghost btn-sm binding-role-clear" onclick="clearBindingRole('${def.id}','${role.key}');return false;">Clear</button>` : ''}
</div>`; </div>`;
} }
......
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