fix(admin): rebalance profiles page hierarchy and add per-profile icons #56
@@ -87,17 +87,26 @@ header.navbar{padding-top:2px!important;padding-bottom:2px!important}
|
|||||||
.navbar-brand a:hover .brand-speedlines line{animation:none}
|
.navbar-brand a:hover .brand-speedlines line{animation:none}
|
||||||
}
|
}
|
||||||
/* ── Profile card specifics ── */
|
/* ── Profile card specifics ── */
|
||||||
.profile-stat{font-size:1.75rem;font-weight:700;line-height:1;font-variant-numeric:tabular-nums}
|
/* Admitted/interactive counts -- compact badge in card header */
|
||||||
.profile-stat-label{font-size:.75rem;color:var(--tblr-secondary)}
|
.profile-admit-badge{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:.82rem}
|
||||||
.profile-field-label{color:var(--tblr-secondary);font-size:.75rem}
|
/* Promoted definition rows -- larger/brighter than the old .78rem muted styling */
|
||||||
.profile-field-val{font-size:.78rem}
|
.profile-field-label{color:var(--tblr-secondary);font-size:.8rem;font-weight:600;letter-spacing:.01em}
|
||||||
|
.profile-field-val{color:rgba(255,255,255,.8);font-size:.92rem}
|
||||||
.profile-zero-alert{margin-left:auto}
|
.profile-zero-alert{margin-left:auto}
|
||||||
.profile-coverage{font-size:.72rem;color:var(--tblr-secondary);margin-bottom:.75rem;line-height:1.35}
|
/* Coverage note -- compact key:value row */
|
||||||
|
.profile-coverage{font-size:.78rem;color:var(--tblr-secondary);margin-bottom:.5rem;line-height:1.3}
|
||||||
|
.profile-coverage .profile-field-label{font-size:.76rem;font-weight:500;letter-spacing:normal}
|
||||||
|
.profile-coverage .profile-field-val{font-size:.8rem;color:var(--tblr-secondary)}
|
||||||
|
.profile-coverage-info{display:inline-flex;align-items:center;margin-left:4px;opacity:.6;cursor:help;vertical-align:middle}
|
||||||
|
.profile-coverage-info:hover{opacity:1}
|
||||||
.empty{color:var(--tblr-secondary);text-align:center;padding:2rem 1rem}
|
.empty{color:var(--tblr-secondary);text-align:center;padding:2rem 1rem}
|
||||||
/* ── Read-only lock hint on built-in cards ── */
|
/* ── Read-only lock hint on built-in cards ── */
|
||||||
.profile-readonly-hint{color:var(--tblr-secondary);font-size:.72rem;display:inline-flex;align-items:center;gap:4px}
|
.profile-readonly-hint{color:var(--tblr-secondary);font-size:.72rem;display:inline-flex;align-items:center;gap:4px}
|
||||||
/* ── Card header actions ── */
|
/* ── Card header actions ── */
|
||||||
.profile-actions{display:flex;align-items:center;gap:6px}
|
.profile-actions{display:flex;align-items:center;gap:6px}
|
||||||
|
/* ââ Profile grid: CSS auto-fill so 5 cards don't leave orphans ââ */
|
||||||
|
#profile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1rem;max-width:1200px;margin-left:auto!important;margin-right:auto!important}
|
||||||
|
#profile-grid>.col-12{grid-column:1/-1}
|
||||||
/* ── Modal glass treatment ── */
|
/* ── Modal glass treatment ── */
|
||||||
.modal-content{
|
.modal-content{
|
||||||
background: rgba(31,41,55,.75);
|
background: rgba(31,41,55,.75);
|
||||||
@@ -317,10 +326,31 @@ function icon(name, size) {
|
|||||||
trash: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
|
trash: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
|
||||||
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
||||||
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
||||||
|
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
|
||||||
|
'map-pin': `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
|
||||||
|
'dollar-sign': `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>`,
|
||||||
|
shield: `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
|
||||||
};
|
};
|
||||||
return svgs[name] || svgs.default;
|
return svgs[name] || svgs.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* âÂÂâ Select a per-profile card-header icon based on definition filters.
|
||||||
|
Precedence (first match wins):
|
||||||
|
1. latency_tolerance set -> clock (time/latency)
|
||||||
|
2. max_cost_per_1m_completion set -> dollar-sign (cost)
|
||||||
|
3. min_tier / max_tier set -> shield (tier/scope)
|
||||||
|
4. provider set OR category-gated -> map-pin (provider/locality)
|
||||||
|
5. no non-null filters -> profile (generic default) âÂÂâ */
|
||||||
|
function profileIcon(def, cov) {
|
||||||
|
def = def || {};
|
||||||
|
if (def.latency_tolerance != null) return 'clock';
|
||||||
|
if (def.max_cost_per_1m_completion != null) return 'dollar-sign';
|
||||||
|
if (def.min_tier != null || def.max_tier != null) return 'shield';
|
||||||
|
if (def.provider != null) return 'map-pin';
|
||||||
|
if (cov && cov.admitting > 0 && cov.admitting < cov.total) return 'map-pin';
|
||||||
|
return 'profile';
|
||||||
|
}
|
||||||
|
|
||||||
function renderStaticIcons() {
|
function renderStaticIcons() {
|
||||||
document.querySelectorAll('[data-icon]').forEach(el => {
|
document.querySelectorAll('[data-icon]').forEach(el => {
|
||||||
el.innerHTML = icon(el.dataset.icon);
|
el.innerHTML = icon(el.dataset.icon);
|
||||||
@@ -475,16 +505,19 @@ function renderProfiles(profiles) {
|
|||||||
const headlineInteractive = gated ? Number(cov.max_interactive_admitted || 0) : interactive;
|
const headlineInteractive = gated ? Number(cov.max_interactive_admitted || 0) : interactive;
|
||||||
const headlineModels = gated ? cov.models : p.admitted_models;
|
const headlineModels = gated ? cov.models : p.admitted_models;
|
||||||
|
|
||||||
|
const admitBadge = `<span class="badge bg-secondary profile-admit-badge" title="${escapeHtml(tooltipForAdmitted(headlineModels))}">${headline.toLocaleString()} admitted · ${headlineInteractive.toLocaleString()} interactive</span>`;
|
||||||
|
|
||||||
const zeroBadge = zeroAdmit
|
const zeroBadge = zeroAdmit
|
||||||
? `<span class="badge bg-warning profile-zero-alert" title="This profile admits zero models for every task category under the live catalog">admits 0 models</span>`
|
? `<span class="badge bg-warning profile-zero-alert" title="This profile admits zero models for every task category under the live catalog">admits 0 models</span>`
|
||||||
: '';
|
: '';
|
||||||
const coverageNote = gated
|
const coverageNote = gated
|
||||||
? `<div class="profile-coverage">
|
? `<div class="profile-coverage row g-0 mb-1">
|
||||||
Category-gated: admits ${headline.toLocaleString()}
|
<div class="col-5 profile-field-label">categories</div>
|
||||||
${headline === 1 ? 'model' : 'models'} for
|
<div class="col-7 profile-field-val">${cov.admitting} of ${cov.total}
|
||||||
${cov.admitting} of ${cov.total} task categories.
|
<span class="profile-coverage-info" title="Category-gated: admits ${headline.toLocaleString()} ${headline === 1 ? 'model' : 'models'} for ${cov.admitting} of ${cov.total} task categories. ${admitted.toLocaleString()} for an unspecified category — its models name the categories they serve, so a category-less probe admits none of them.">
|
||||||
<span class="text-muted">${admitted.toLocaleString()} for an unspecified category —
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
its models name the categories they serve, so a category-less probe admits none of them.</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
const readonlyHint = isBuiltin
|
const readonlyHint = isBuiltin
|
||||||
@@ -502,15 +535,16 @@ function renderProfiles(profiles) {
|
|||||||
<button class="btn btn-outline-secondary btn-sm" title="Edit profile" data-action="edit" data-profile-name="${name}"><span data-icon="edit"></span></button>
|
<button class="btn btn-outline-secondary btn-sm" title="Edit profile" data-action="edit" data-profile-name="${name}"><span data-icon="edit"></span></button>
|
||||||
<button class="btn btn-danger btn-sm" title="Delete profile" data-action="delete" data-profile-name="${name}"><span data-icon="trash"></span></button>
|
<button class="btn btn-danger btn-sm" title="Delete profile" data-action="delete" data-profile-name="${name}"><span data-icon="trash"></span></button>
|
||||||
</div>`;
|
</div>`;
|
||||||
return `<div class="col-md-6 col-xl-4" data-profile-name="${name}">
|
const cardIcon = profileIcon(p.definition, cov);
|
||||||
|
return `<div data-profile-name="${name}">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span data-icon="profile"></span>
|
<span data-icon="${cardIcon}"></span>
|
||||||
<h3 class="card-title mb-0">${escapeHtml(String(p.name || '—'))}</h3>
|
<h3 class="card-title mb-0">${escapeHtml(String(p.name || '—'))}</h3>
|
||||||
</div>
|
</div>
|
||||||
${zeroBadge}
|
<div class="d-flex align-items-center gap-2">${admitBadge}${zeroBadge}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center justify-content-between mt-1">
|
<div class="d-flex align-items-center justify-content-between mt-1">
|
||||||
<div class="card-subtitle text-muted">${sourceBadge(p.source)} ${readonlyHint}</div>
|
<div class="card-subtitle text-muted">${sourceBadge(p.source)} ${readonlyHint}</div>
|
||||||
@@ -518,18 +552,8 @@ function renderProfiles(profiles) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 mb-2">
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="profile-stat" title="${escapeHtml(tooltipForAdmitted(headlineModels))}">${headline.toLocaleString()}</div>
|
|
||||||
<div class="profile-stat-label">admitted models</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="profile-stat">${headlineInteractive.toLocaleString()}</div>
|
|
||||||
<div class="profile-stat-label">interactive models</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${coverageNote}
|
|
||||||
${formatDefinition(p.definition)}
|
${formatDefinition(p.definition)}
|
||||||
|
${coverageNote}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user