fffiloni commited on
Commit
774d50c
·
verified ·
1 Parent(s): 7d769bd

Upload 7 files

Browse files
Files changed (2) hide show
  1. web/static/app.css +164 -24
  2. web/static/app.js +33 -16
web/static/app.css CHANGED
@@ -2094,30 +2094,170 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
2094
  .eval-enable-button[hidden]{display:none!important;}
2095
  .eval-enable-button{white-space:nowrap;}
2096
 
2097
- /* v182: Run Storage / Eval Archive split section */
2098
  .compact-storage{gap:12px!important;}
2099
  .compact-storage .bucket-status-card{margin-bottom:0!important;}
2100
- .eval-archive-card{
2101
- display:grid;
2102
- grid-template-columns:minmax(0,1fr) auto;
2103
- align-items:center;
2104
- gap:12px;
2105
- margin-top:4px;
2106
- padding:12px 12px;
2107
- border-radius:18px;
2108
- background:linear-gradient(180deg,rgba(255,255,255,.78),rgba(248,250,252,.7));
2109
- border:1px solid rgba(226,232,240,.72);
2110
- box-shadow:0 1px 0 rgba(255,255,255,.9) inset;
2111
- }
2112
- .eval-archive-card[hidden]{display:none!important;}
2113
- .eval-archive-card.on{background:linear-gradient(180deg,rgba(236,253,245,.66),rgba(255,255,255,.78));border-color:rgba(34,197,94,.18)}
2114
- .eval-archive-card.off{background:linear-gradient(180deg,rgba(248,250,252,.78),rgba(255,255,255,.8));}
2115
- .eval-archive-copy{min-width:0;display:grid;gap:3px;}
2116
- .eval-archive-copy strong{font-size:.88rem;line-height:1.15;color:var(--ink);letter-spacing:-.01em;}
2117
- .eval-archive-copy p{margin:0;color:var(--muted);font-size:.72rem;line-height:1.25;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
2118
- .eval-archive-card.on .eval-archive-copy p{color:#047857;}
2119
- .eval-archive-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap;}
2120
- .eval-archive-actions button{min-height:34px;padding:8px 10px!important;border-radius:12px!important;white-space:nowrap;}
2121
  .eval-archive-actions .danger{color:#be123c!important;background:rgba(255,241,242,.82)!important;border-color:rgba(244,63,94,.2)!important;}
2122
- .eval-feedback{display:block!important;width:auto!important;max-width:100%;padding:0!important;border:0!important;background:transparent!important;border-radius:0!important;font-size:.72rem!important;font-weight:650!important;line-height:1.25!important;letter-spacing:0!important;}
2123
- @media(max-width:760px){.eval-archive-card{grid-template-columns:1fr}.eval-archive-actions{justify-content:flex-start}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2094
  .eval-enable-button[hidden]{display:none!important;}
2095
  .eval-enable-button{white-space:nowrap;}
2096
 
2097
+ /* v183: Eval Archive moved to the observability column under Run Stats */
2098
  .compact-storage{gap:12px!important;}
2099
  .compact-storage .bucket-status-card{margin-bottom:0!important;}
2100
+ .eval-archive-panel{
2101
+ margin-top:14px;
2102
+ padding:14px;
2103
+ border-radius:20px;
2104
+ background:linear-gradient(180deg,rgba(255,255,255,.92),rgba(248,250,252,.76));
2105
+ border:1px solid rgba(226,232,240,.78);
2106
+ box-shadow:0 16px 36px rgba(15,23,42,.035),0 1px 0 rgba(255,255,255,.92) inset;
2107
+ }
2108
+ .eval-archive-panel[hidden]{display:none!important;}
2109
+ .eval-archive-panel.on{background:linear-gradient(180deg,rgba(240,253,244,.82),rgba(255,255,255,.86));border-color:rgba(34,197,94,.2)}
2110
+ .eval-archive-panel.off{background:linear-gradient(180deg,rgba(248,250,252,.88),rgba(255,255,255,.86));}
2111
+ .eval-archive-title-row{align-items:flex-start;margin-bottom:8px!important;}
2112
+ .eval-archive-title-row h4{margin:2px 0 0;font-size:.98rem;line-height:1.15;letter-spacing:-.02em;color:var(--ink);}
2113
+ .eval-archive-summary{margin:0 0 12px;color:var(--muted);font-size:.78rem;line-height:1.35;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
2114
+ .eval-archive-panel.on .eval-archive-summary{color:#047857;}
2115
+ .eval-archive-actions{display:flex;align-items:center;justify-content:flex-start;gap:8px;flex-wrap:wrap;}
2116
+ .eval-archive-actions button{min-height:34px;padding:8px 11px!important;border-radius:12px!important;white-space:nowrap;}
 
 
 
 
2117
  .eval-archive-actions .danger{color:#be123c!important;background:rgba(255,241,242,.82)!important;border-color:rgba(244,63,94,.2)!important;}
2118
+ .eval-feedback{display:block!important;width:auto!important;max-width:100%;padding:0!important;border:0!important;background:transparent!important;border-radius:0!important;font-size:.78rem!important;font-weight:650!important;line-height:1.35!important;letter-spacing:0!important;}
2119
+ .eval-feedback.off{color:#64748b!important;background:transparent!important;border:0!important;opacity:1!important}
2120
+ @media(max-width:760px){.eval-archive-actions{justify-content:flex-start}}
2121
+
2122
+ /* v184: compact premium Model pre-scan states */
2123
+ .model-prescan-card.required{
2124
+ border-color:rgba(148,163,184,.28)!important;
2125
+ background:radial-gradient(circle at 12% 0%,rgba(59,130,246,.075),transparent 36%),linear-gradient(180deg,rgba(255,255,255,.99),rgba(248,250,252,.94))!important;
2126
+ box-shadow:0 14px 34px rgba(15,23,42,.055),0 1px 0 rgba(255,255,255,.9) inset!important;
2127
+ }
2128
+ .model-prescan-card.required .model-prescan-head strong{font-size:16px!important;letter-spacing:-.03em!important}
2129
+ .model-prescan-card.required>p{font-size:12.75px!important;line-height:1.42!important;max-width:50ch!important}
2130
+ .model-prescan-card.required #scanModel{
2131
+ border-color:rgba(37,99,235,.22)!important;
2132
+ background:linear-gradient(180deg,#fff,#f8fafc)!important;
2133
+ box-shadow:0 8px 18px rgba(15,23,42,.06)!important;
2134
+ }
2135
+ .model-prescan-card.success{
2136
+ border-color:rgba(34,197,94,.20)!important;
2137
+ background:radial-gradient(circle at 98% 0%,rgba(34,197,94,.08),transparent 34%),linear-gradient(180deg,rgba(255,255,255,.99),rgba(248,250,252,.93))!important;
2138
+ }
2139
+ .model-prescan-card.success .model-prescan-head strong{font-size:15px!important}
2140
+ .model-prescan-card.success>p{font-size:12.5px!important;line-height:1.36!important;margin-top:-2px!important}
2141
+ .model-prescan-card.success .model-prescan-signals{gap:8px!important}
2142
+ .prescan-compact-grid{
2143
+ display:grid!important;
2144
+ grid-template-columns:repeat(3,minmax(0,1fr))!important;
2145
+ gap:7px!important;
2146
+ }
2147
+ .prescan-chip{
2148
+ display:grid!important;
2149
+ grid-template-columns:auto 1fr!important;
2150
+ grid-template-areas:"icon label" "icon value"!important;
2151
+ column-gap:7px!important;
2152
+ align-items:center!important;
2153
+ min-width:0!important;
2154
+ padding:8px 9px!important;
2155
+ border-radius:14px!important;
2156
+ border:1px solid rgba(226,232,240,.82)!important;
2157
+ background:rgba(255,255,255,.82)!important;
2158
+ }
2159
+ .prescan-chip i{
2160
+ grid-area:icon!important;
2161
+ width:23px!important;
2162
+ height:23px!important;
2163
+ display:inline-flex!important;
2164
+ align-items:center!important;
2165
+ justify-content:center!important;
2166
+ border-radius:999px!important;
2167
+ background:rgba(248,250,252,.96)!important;
2168
+ color:#475569!important;
2169
+ font-style:normal!important;
2170
+ font-size:12px!important;
2171
+ font-weight:900!important;
2172
+ }
2173
+ .prescan-chip em{
2174
+ grid-area:label!important;
2175
+ font-size:8.5px!important;
2176
+ line-height:1!important;
2177
+ letter-spacing:.13em!important;
2178
+ text-transform:uppercase!important;
2179
+ color:#64748b!important;
2180
+ font-style:normal!important;
2181
+ font-weight:850!important;
2182
+ }
2183
+ .prescan-chip strong{
2184
+ grid-area:value!important;
2185
+ min-width:0!important;
2186
+ overflow:hidden!important;
2187
+ text-overflow:ellipsis!important;
2188
+ white-space:nowrap!important;
2189
+ color:#0f172a!important;
2190
+ font-size:11.5px!important;
2191
+ line-height:1.15!important;
2192
+ }
2193
+ .prescan-chip.good{border-color:rgba(34,197,94,.18)!important;background:rgba(240,253,244,.56)!important}
2194
+ .prescan-chip.good i{background:rgba(220,252,231,.95)!important;color:#047857!important}
2195
+ .prescan-pill-row{display:flex!important;flex-wrap:wrap!important;gap:6px!important;min-width:0!important}
2196
+ .prescan-pill{
2197
+ display:inline-flex!important;
2198
+ align-items:center!important;
2199
+ gap:5px!important;
2200
+ max-width:100%!important;
2201
+ padding:5px 8px!important;
2202
+ border-radius:999px!important;
2203
+ border:1px solid rgba(226,232,240,.86)!important;
2204
+ background:rgba(255,255,255,.76)!important;
2205
+ color:#334155!important;
2206
+ font-size:10.75px!important;
2207
+ line-height:1.2!important;
2208
+ white-space:nowrap!important;
2209
+ overflow:hidden!important;
2210
+ text-overflow:ellipsis!important;
2211
+ }
2212
+ .prescan-pill.good{border-color:rgba(34,197,94,.18)!important;background:rgba(240,253,244,.68)!important}
2213
+ .prescan-pill.risk{border-color:rgba(245,158,11,.25)!important;background:rgba(255,251,235,.72)!important}
2214
+ .prescan-pill .prescan-icon{font-weight:900!important;opacity:.8!important}
2215
+ .prescan-recommendation-line{
2216
+ display:flex!important;
2217
+ align-items:center!important;
2218
+ gap:7px!important;
2219
+ padding:7px 9px!important;
2220
+ border-radius:13px!important;
2221
+ border:1px solid rgba(226,232,240,.82)!important;
2222
+ background:rgba(255,255,255,.72)!important;
2223
+ color:#475569!important;
2224
+ font-size:11px!important;
2225
+ line-height:1.25!important;
2226
+ }
2227
+ .prescan-recommendation-line b{color:#0f172a!important;font-size:10px!important;text-transform:uppercase!important;letter-spacing:.11em!important}
2228
+ .model-prescan-card.success #scanModel{padding:7px 10px!important;font-size:11.5px!important;border-radius:12px!important}
2229
+ @media(max-width:720px){.prescan-compact-grid{grid-template-columns:1fr!important}.prescan-pill{white-space:normal!important}}
2230
+
2231
+ /* v185: consistent compact Model pre-scan rendering for all states */
2232
+ .model-prescan-card.required{padding:14px!important;gap:10px!important}
2233
+ .model-prescan-card.required .model-prescan-signals{display:block!important;margin-top:1px!important}
2234
+ .model-prescan-card.running{
2235
+ border-color:rgba(37,99,235,.20)!important;
2236
+ background:radial-gradient(circle at 96% 0%,rgba(59,130,246,.09),transparent 34%),linear-gradient(180deg,rgba(255,255,255,.99),rgba(248,250,252,.94))!important;
2237
+ }
2238
+ .model-prescan-card.warn{
2239
+ border-color:rgba(245,158,11,.22)!important;
2240
+ background:radial-gradient(circle at 96% 0%,rgba(245,158,11,.10),transparent 34%),linear-gradient(180deg,rgba(255,255,255,.99),rgba(255,251,235,.52))!important;
2241
+ }
2242
+ .model-prescan-card.error{
2243
+ border-color:rgba(244,63,94,.20)!important;
2244
+ background:radial-gradient(circle at 96% 0%,rgba(244,63,94,.09),transparent 34%),linear-gradient(180deg,rgba(255,255,255,.99),rgba(255,241,242,.52))!important;
2245
+ }
2246
+ .model-prescan-card.running .model-prescan-head strong,
2247
+ .model-prescan-card.warn .model-prescan-head strong,
2248
+ .model-prescan-card.error .model-prescan-head strong{font-size:15px!important;letter-spacing:-.025em!important}
2249
+ .model-prescan-card.warn .prescan-chip,
2250
+ .model-prescan-card.error .prescan-chip,
2251
+ .model-prescan-card.running .prescan-chip{background:rgba(255,255,255,.78)!important}
2252
+ .prescan-pill.running{border-color:rgba(37,99,235,.18)!important;background:rgba(239,246,255,.72)!important;color:#1e3a8a!important}
2253
+ .prescan-recommendation-line.warn{border-color:rgba(245,158,11,.18)!important;background:rgba(255,251,235,.70)!important}
2254
+ .prescan-recommendation-line.error{border-color:rgba(244,63,94,.18)!important;background:rgba(255,241,242,.70)!important}
2255
+ .prescan-recommendation-line.running{border-color:rgba(37,99,235,.16)!important;background:rgba(239,246,255,.62)!important}
2256
+ .prescan-hints{margin-top:2px!important}
2257
+ .prescan-spinner{display:inline-flex!important;animation:prescan-spin 1.1s linear infinite!important}
2258
+ @keyframes prescan-spin{to{transform:rotate(360deg)}}
2259
+ .model-prescan-card.warn #scanModel,
2260
+ .model-prescan-card.error #scanModel,
2261
+ .model-prescan-card.running #scanModel{padding:7px 10px!important;font-size:11.5px!important;border-radius:12px!important}
2262
+ .model-prescan-card.error #scanModel{border-color:rgba(244,63,94,.18)!important;background:linear-gradient(180deg,#fff,#fff7f7)!important}
2263
+ @media(prefers-reduced-motion:reduce){.prescan-spinner{animation:none!important}}
web/static/app.js CHANGED
@@ -229,6 +229,7 @@ function renderAnonymousEvalStatus(me){
229
  const enable=$('enableEvalArchive');
230
  const disable=$('disableEvalArchive');
231
  const verify=$('verifyEvalArchive');
 
232
  const cfg=me?.anonymous_eval||{};
233
  const enabled=Boolean(cfg.enabled);
234
  const canManage=Boolean(cfg.can_manage)&&cfg.env_override!==true;
@@ -238,16 +239,20 @@ function renderAnonymousEvalStatus(me){
238
  const mount=cfg.job_mount_path||cfg.default_mount_path||'/evals';
239
  if(card){
240
  card.hidden=false;
241
- card.className='eval-archive-card '+(enabled?'on':'off')+(readonly?' readonly':'')+(canManage?' manageable':'');
242
  }
243
  if(title){
244
- title.textContent=enabled?'Anonymous eval archive is on':'Anonymous eval archive is off';
 
 
 
 
245
  }
246
  if(text){
247
  text.className='eval-feedback '+(enabled?'on':'off');
248
  if(enabled){
249
  const manager=cfg.managed_by==='you'?'You manage this instance setting.':`Managed by ${cfg.managed_by||'instance owner'}.`;
250
- text.textContent=`${bucket}${path} → ${mount}`;
251
  text.title=`Anonymous project eval archive enabled from ${cfg.source||'config'}: ${bucket}${path} mounted at ${mount}. ${manager} ASF backend publishes anonymized eval records after user Jobs write local run evals.`;
252
  }else if(me?.username){
253
  text.textContent='Mount an operator eval bucket at /evals, then enable publishing.';
@@ -299,31 +304,45 @@ function renderModelPreScan(scan=null){
299
  const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
300
  if(!card)return;
301
  const current=normalizeModelInput(formValue('modelId'));
302
- const matches=scan&&scan.ok&&normalizeModelInput(scan.model_id)===current;
 
303
  if(!scan||!matches){
304
  card.className='model-prescan-card required';
305
  setText('modelPreScanTitle','Pre-scan required');
306
- setText('modelPreScanSummary','Run a fast metadata scan before launching a paid Job. It checks the model card guidance, runnable examples, Diffusers signals, files, safetensors, gated access and custom-code risk without downloading weights.');
307
  if(badge){badge.textContent='Required';badge.className='badge neutral'}
308
- if(signals){signals.hidden=true;signals.innerHTML=''}
309
  if(ackWrap)ackWrap.hidden=true;
310
  return;
311
  }
312
  const verdict=String(scan.verdict||'caution').toLowerCase();
313
  const cls=scanVerdictClass(verdict);
 
314
  card.className='model-prescan-card '+cls;
315
- setText('modelPreScanTitle',`${verdict.charAt(0).toUpperCase()+verdict.slice(1)} · score ${scan.score ?? '—'}/100`);
 
316
  setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.');
317
- if(badge){badge.textContent=verdict;badge.className='badge '+cls}
318
- const good=(scan.good_signals||[]).slice(0,3);
319
  const risk=(scan.risk_signals||[]).slice(0,4);
320
  const outputType=outputTypeFromModelScan(scan);
321
  const recs=[...(scan.recommendations||[]).slice(0,2)];
322
- if(outputType)recs.unshift(`Expected output: ${outputType}`);
323
  if(signals){
324
- const modelMeta=[scan.pipeline_tag?['Pipeline',scan.pipeline_tag]:null,scan.library_name?['Library',scan.library_name]:null,outputType?['Output',outputType]:null].filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
325
  signals.hidden=false;
326
- signals.innerHTML=`<div class="prescan-meta">${modelMeta.map(([k,v])=>`<span><em>${escapeHtml(k)}</em><strong>${escapeHtml(String(v))}</strong></span>`).join('')}</div>${good.length?`<div><strong>Good signals</strong>${good.map(x=>`<span class="good">${escapeHtml(x)}</span>`).join('')}</div>`:''}${risk.length?`<div><strong>Watch</strong>${risk.map(x=>`<span class="risk">${escapeHtml(x)}</span>`).join('')}</div>`:''}${recs.length?`<div><strong>Recommendation</strong>${recs.map(x=>`<span>${escapeHtml(x)}</span>`).join('')}</div>`:''}`;
327
  }
328
  if(ackWrap)ackWrap.hidden=!(verdict==='risky');
329
  }
@@ -333,9 +352,7 @@ async function scanModel(){
333
  const model=formValue('modelId');
334
  if(!model){showMessage('Enter a model card URL or model ID before scanning.',true);return}
335
  setButtonBusy('scanModel',true,'Scanning…');
336
- const badge=$('modelPreScanBadge');if(badge){badge.textContent='Scanning';badge.className='badge running'}
337
- setText('modelPreScanTitle','Scanning model metadata…');
338
- setText('modelPreScanSummary','Reading the model card, Hub metadata and small config files. We do not download model weights.');
339
  try{
340
  const result=await apiPost('/api/models/pre-scan',{model_id:model});
341
  state.modelScan=result;
@@ -344,7 +361,7 @@ async function scanModel(){
344
  renderModelPreScan(result);
345
  setBuildLaunchEnabled();
346
  showMessage(`Model pre-scan: ${result.verdict} (${result.score}/100)`,result.verdict==='safe'?'success':(result.verdict==='unsupported'||result.verdict==='risky'?true:'warning'));
347
- }catch(e){state.modelScan=null;renderModelPreScan(null);showMessage(e.message,true)}
348
  finally{setButtonBusy('scanModel',false);setBuildLaunchEnabled()}
349
  }
350
 
 
229
  const enable=$('enableEvalArchive');
230
  const disable=$('disableEvalArchive');
231
  const verify=$('verifyEvalArchive');
232
+ const stateBadge=$('evalArchiveStateBadge');
233
  const cfg=me?.anonymous_eval||{};
234
  const enabled=Boolean(cfg.enabled);
235
  const canManage=Boolean(cfg.can_manage)&&cfg.env_override!==true;
 
239
  const mount=cfg.job_mount_path||cfg.default_mount_path||'/evals';
240
  if(card){
241
  card.hidden=false;
242
+ card.className='eval-archive-panel '+(enabled?'on':'off')+(readonly?' readonly':'')+(canManage?' manageable':'');
243
  }
244
  if(title){
245
+ title.textContent=enabled?'Anonymous eval archive':'Anonymous eval archive';
246
+ }
247
+ if(stateBadge){
248
+ stateBadge.textContent=enabled?'On':'Off';
249
+ stateBadge.className='badge '+(enabled?'success':'neutral');
250
  }
251
  if(text){
252
  text.className='eval-feedback '+(enabled?'on':'off');
253
  if(enabled){
254
  const manager=cfg.managed_by==='you'?'You manage this instance setting.':`Managed by ${cfg.managed_by||'instance owner'}.`;
255
+ text.textContent=`On · ${bucket}${path} → ${mount}`;
256
  text.title=`Anonymous project eval archive enabled from ${cfg.source||'config'}: ${bucket}${path} mounted at ${mount}. ${manager} ASF backend publishes anonymized eval records after user Jobs write local run evals.`;
257
  }else if(me?.username){
258
  text.textContent='Mount an operator eval bucket at /evals, then enable publishing.';
 
304
  const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap');
305
  if(!card)return;
306
  const current=normalizeModelInput(formValue('modelId'));
307
+ const isTransient=scan&&scan.transient===true;
308
+ const matches=scan&&((scan.ok&&normalizeModelInput(scan.model_id)===current)||isTransient);
309
  if(!scan||!matches){
310
  card.className='model-prescan-card required';
311
  setText('modelPreScanTitle','Pre-scan required');
312
+ setText('modelPreScanSummary','Run a fast metadata scan before launching a paid Job. It checks model card guidance, runnable examples, Diffusers signals, files, safetensors, gated access and custom-code risk without downloading weights.');
313
  if(badge){badge.textContent='Required';badge.className='badge neutral'}
314
+ if(signals){signals.hidden=false;signals.innerHTML='<div class="prescan-pill-row prescan-hints"><span class="prescan-pill"><b class="prescan-icon">↗</b>Metadata only</span><span class="prescan-pill"><b class="prescan-icon">◇</b>No weight download</span><span class="prescan-pill"><b class="prescan-icon">✓</b>Required before Job</span></div>'}
315
  if(ackWrap)ackWrap.hidden=true;
316
  return;
317
  }
318
  const verdict=String(scan.verdict||'caution').toLowerCase();
319
  const cls=scanVerdictClass(verdict);
320
+ const labelMap={safe:'Safe',caution:'Needs review',risky:'Risky',unsupported:'Unsupported',scanning:'Scanning',error:'Scan failed'};
321
  card.className='model-prescan-card '+cls;
322
+ const scoreText=(scan.score===undefined||scan.score===null||verdict==='scanning'||verdict==='error')?'':` · score ${scan.score}/100`;
323
+ setText('modelPreScanTitle',`${labelMap[verdict]||labelMap.caution}${scoreText}`);
324
  setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.');
325
+ if(badge){badge.textContent=labelMap[verdict]||verdict;badge.className='badge '+cls}
326
+ const good=(scan.good_signals||[]).slice(0,4);
327
  const risk=(scan.risk_signals||[]).slice(0,4);
328
  const outputType=outputTypeFromModelScan(scan);
329
  const recs=[...(scan.recommendations||[]).slice(0,2)];
330
+ if(outputType&&!recs.some(x=>String(x).toLowerCase().includes('expected output')))recs.unshift(`Expected output: ${outputType}`);
331
  if(signals){
332
+ const metaCards=[
333
+ outputType?['↳','Output',outputType,verdict==='safe']:null,
334
+ scan.pipeline_tag?['◈','Pipeline',scan.pipeline_tag,false]:null,
335
+ scan.library_name?['◎','Library',scan.library_name,false]:null,
336
+ ].filter(Boolean).slice(0,3);
337
+ const compactMeta=metaCards.length?`<div class="prescan-meta prescan-compact-grid">${metaCards.map(([icon,k,v,isGood])=>`<span class="prescan-chip ${isGood?'good':''}"><i>${escapeHtml(icon)}</i><em>${escapeHtml(k)}</em><strong>${escapeHtml(String(v))}</strong></span>`).join('')}</div>`:'';
338
+ const goodPills=good.length?`<div class="prescan-pill-row">${good.map(x=>`<span class="prescan-pill good"><b class="prescan-icon">✓</b>${escapeHtml(x)}</span>`).join('')}</div>`:'';
339
+ const riskPills=risk.length?`<div class="prescan-pill-row">${risk.map(x=>`<span class="prescan-pill risk"><b class="prescan-icon">!</b>${escapeHtml(x)}</span>`).join('')}</div>`:'';
340
+ const runningPills=verdict==='scanning'?'<div class="prescan-pill-row"><span class="prescan-pill running"><b class="prescan-icon prescan-spinner">◌</b>Reading model card</span><span class="prescan-pill running"><b class="prescan-icon">◇</b>Checking metadata</span></div>':'';
341
+ const errorPill=verdict==='error'?`<div class="prescan-pill-row"><span class="prescan-pill risk"><b class="prescan-icon">!</b>${escapeHtml(scan.error||'The scan could not complete. Try again.')}</span></div>`:'';
342
+ const recLine=recs.length?`<div class="prescan-recommendation-line ${cls}"><b>Recommendation</b><span>${escapeHtml(recs[0])}${recs.length>1?` · ${escapeHtml(recs.slice(1).join(' · '))}`:''}</span></div>`:'';
343
+ const emptyHint=(!compactMeta&&!goodPills&&!riskPills&&!runningPills&&!errorPill&&!recLine)?'<div class="prescan-pill-row"><span class="prescan-pill"><b class="prescan-icon">◇</b>No extra metadata found.</span></div>':'';
344
  signals.hidden=false;
345
+ signals.innerHTML=`${compactMeta}${runningPills}${goodPills}${riskPills}${errorPill}${recLine}${emptyHint}`;
346
  }
347
  if(ackWrap)ackWrap.hidden=!(verdict==='risky');
348
  }
 
352
  const model=formValue('modelId');
353
  if(!model){showMessage('Enter a model card URL or model ID before scanning.',true);return}
354
  setButtonBusy('scanModel',true,'Scanning…');
355
+ renderModelPreScan({ok:true,transient:true,model_id:model,verdict:'scanning',summary:'Reading the model card, Hub metadata and small config files. We do not download model weights.'});
 
 
356
  try{
357
  const result=await apiPost('/api/models/pre-scan',{model_id:model});
358
  state.modelScan=result;
 
361
  renderModelPreScan(result);
362
  setBuildLaunchEnabled();
363
  showMessage(`Model pre-scan: ${result.verdict} (${result.score}/100)`,result.verdict==='safe'?'success':(result.verdict==='unsupported'||result.verdict==='risky'?true:'warning'));
364
+ }catch(e){state.modelScan=null;renderModelPreScan({ok:true,transient:true,model_id:model,verdict:'error',summary:'The model pre-scan could not complete. Fix the issue or try again before launching a Job.',error:e.message});showMessage(e.message,true)}
365
  finally{setButtonBusy('scanModel',false);setBuildLaunchEnabled()}
366
  }
367