1// ââââââââââââââââââââââââââââââââââââââ 2// TAB 5: Bot Library (Botåº) â API-driven 3// ââââââââââââââââââââââââââââââââââââââ 4const PAGE_SIZE = 50; 5 6const API_SORT_OPTS = [ 7 {val:'chat_imp', label:'Chat/Imp'}, 8 {val:'feed_ctr', label:'Feed CTR'}, 9 {val:'shows', label:'æå é'}, 10 {val:'chats', label:'è天æ°'}, 11 {val:'chat_users', label:'èå¤©ç¨æ·'}, 12 {val:'ctr', label:'æ»CTR'}, 13 {val:'open_rate', label:'å¼èç'}, 14 {val:'depth', label:'è天深度'}, 15 {val:'retention1', label:'次æ¥çå'}, 16 {val:'retention7', label:'7æ¥çå'}, 17 {val:'return_rate', label:'å访ç'}, 18]; 19 20function renderApiExplorer() { 21 if (!apiBotsResult) return; 22 const c = document.getElementById('view-bots'); 23 const { total, page, page_size, total_pages, bots } = apiBotsResult; 24 const { date_from, date_to } = apiDateRange(); 25 const isCustom = apiBotsState.dateRange === 'custom'; 26 27 let h = ''; 28 h += `<div class="page-title">Botåº</div>`; 29 h += `<div class="page-subtitle">宿¶æ°æ® â ${total.toLocaleString()} bots · ${date_from} â ${date_to}</div>`; 30 31 // Controls bar 32 h += `<div class="filter-bar" style="flex-wrap:wrap;gap:8px;">`; 33 // Date range picker 34 h += `<div style="display:flex;gap:4px;align-items:center;">`; 35 ['7d','30d','custom'].forEach(r => { 36 h += `<button class="page-btn${apiBotsState.dateRange===r?' active':''}" data-range="${r}" style="min-width:44px">${r}</button>`; 37 }); 38 if (isCustom) { 39 h += `<input type="date" id="apiDateFrom" value="${apiBotsState.date_from||date_from}" style="background:#2a2a28;border:1px solid rgba(255,255,255,0.12);border-radius:6px;color:rgba(255,255,255,0.7);padding:4px 8px;font-size:12px;">`; 40 h += `<span style="color:rgba(255,255,255,0.3)">â</span>`; 41 h += `<input type="date" id="apiDateTo" value="${apiBotsState.date_to||date_to}" style="background:#2a2a28;border:1px solid rgba(255,255,255,0.12);border-radius:6px;color:rgba(255,255,255,0.7);padding:4px 8px;font-size:12px;">`; 42 h += `<button class="page-btn" id="apiDateApply" style="background:rgba(56,189,248,0.15);border-color:rgba(56,189,248,0.3)">Apply</button>`; 43 } 44 h += `</div>`; 45 // Tag filter 46 h += `<select class="filter-select" id="apiTagFilter" style="min-width:160px"><option value="">æ ç¾ (All)</option>`; 47 apiTags.forEach(t => { h += `<option value="${t.tag}" ${apiBotsState.tag===t.tag?'selected':''}>${t.tag} (${t.bot_count.toLocaleString()})</option>`; }); 48 h += `</select>`; 49 // Sort 50 h += `<select class="filter-select" id="apiSortBy">`; 51 API_SORT_OPTS.forEach(o => { h += `<option value="${o.val}" ${apiBotsState.sort_by===o.val?'selected':''}>${o.label}</option>`; }); 52 h += `</select>`; 53 h += `<select class="filter-select" id="apiSortOrder" style="width:80px"> 54 <option value="desc" ${apiBotsState.sort_order==='desc'?'selected':''}>â</option> 55 <option value="asc" ${apiBotsState.sort_order==='asc'?'selected':''}>â</option> 56 </select>`; 57 // Search 58 h += `<input type="text" class="search-input" placeholder="æç´¢æ é¢..." id="apiSearch" value="${apiBotsState.search}" style="flex:1;min-width:160px">`; 59 h += `</div>`; 60 61 // Helper: compute ratio from raw numbers (always matches displayed values) 62 const ratio = (a, b, dp=4) => (b && b > 0) ? (a / b) : null; 63 const fmtN = (n) => n != null && n > 0 ? n.toLocaleString() : 'â'; 64 const fmtR = (v, dp=2) => v != null ? (v * 100).toFixed(dp) + '%' : 'â'; 65 const fmtD = (v, dp=1) => v != null ? v.toFixed(dp) : 'â'; 66 67 // Table â Jay's requested metrics, all verifiable from raw numbers 68 const sortCols = [ 69 {key:'_rank',label:'#',sort:null}, 70 {key:'title',label:'Bot',sort:null}, 71 {key:'feed_shows',label:'Feedæå ',sort:'shows'}, 72 {key:'feed_clicks',label:'Feedç¹å»',sort:'clicks'}, 73 {key:'chats',label:'æ»Chat',sort:'chats'}, 74 {key:'feed_ctr',label:'FeedCTR',sort:'feed_ctr'}, 75 {key:'chat_per_imp',label:'Chat/Imp',sort:'chat_imp'}, 76 {key:'feed_open_rate',label:'Feedå¼èç',sort:'open_rate'}, 77 {key:'depth',label:'äººåæ·±åº¦',sort:'depth'}, 78 {key:'chat_users',label:'è天UV',sort:'chat_users'}, 79 {key:'tag',label:'æ ç¾',sort:null}, 80 ]; 81 82 h += `<div class="table-wrap"><table><thead><tr>`; 83 for (const col of sortCols) { 84 const isActive = col.sort && apiBotsState.sort_by === col.sort; 85 const arrow = isActive ? (apiBotsState.sort_order==='asc'?'â²':'â¼') : 'â²'; 86 h += `<th class="${col.sort?'sortable':''} ${isActive?'sort-active':''}" data-sortkey="${col.sort||''}">${col.label}${col.sort?`<span class="sort-arrow">${arrow}</span>`:''}</th>`; 87 } 88 h += `</tr></thead><tbody>`; 89 90 const startRank = (page - 1) * page_size;
91 bots.forEach((b, idx) => { 92 const rank = startRank + idx + 1; 93 const thumb = b.thumbnail_url ? `<img src="${b.thumbnail_url}" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0;background:#2a2a28;" loading="lazy" onerror="this.style.display='none'">` : ''; 94 const title = (b.title || b.prompt_id).replace(/"/g, '"'); 95 // Raw numbers from API 96 const feedShows = b.home_shows_sum || b.feed_shows || 0; 97 const feedClicks = b.home_clicks_sum || b.feed_clicks || 0; 98 const totalChats = b.chats_sum || 0; 99 const chatUV = b.chat_users || 0; 100 const feedClickUV = b.home_click_users || b.feed_click_uv || 0; 101 const feedChatUV = b.home_chat_users || b.feed_chat_uv || 0; 102 // Ratios computed from raw numbers â always verifiable 103 const feedCTR = ratio(feedClicks, feedShows); 104 // chat/imp å = å端 chat_imp (home_chats / home_shows, feed CPI). è·å端 105 // chat_imp æåºå¯¹é½ â ä¹åç¨ ratio(chats_sum, home_shows_sum) æ¯æ··æå¼ 106 // (å ¨æ¸ é chat / feed shows) è·æåºä¸ä¸è´, çèµ·æ¥"æåºä¹±". 107 const feedChats = b.home_chats_sum || b.feed_chats || 0; 108 const chatPerImp = b.chat_imp != null ? b.chat_imp : ratio(feedChats, feedShows); 109 const feedOpenRate = ratio(feedChatUV, feedClickUV); 110 const depth = ratio(totalChats, chatUV); 111 h += `<tr class="clickable-row" data-botid="${b.prompt_id}"> 112 <td class="mono" style="color:rgba(255,255,255,0.2)">${rank}</td> 113 <td class="title-cell"><div style="display:flex;align-items:center;gap:8px;">${thumb}<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${title}">${b.title||b.prompt_id}</span></div></td> 114 <td class="mono">${fmtN(feedShows)}</td> 115 <td class="mono">${fmtN(feedClicks)}</td> 116 <td class="mono">${fmtN(totalChats)}</td> 117 <td class="mono">${fmtR(feedCTR)}</td> 118 <td class="mono" style="color:#3a86ff;font-weight:500">${fmtD(chatPerImp,2)}</td> 119 <td class="mono">${fmtR(feedOpenRate)}</td> 120 <td class="mono">${fmtD(depth,1)}</td> 121 <td class="mono">${fmtN(chatUV)}</td> 122 <td style="font-size:11px;color:rgba(255,255,255,0.35)">${b.v8_primary_tag||b.tag||''}</td> 123 </tr>`; 124 }); 125 h += `</tbody></table></div>`; 126 127 // Pagination 128 h += `<div class="pagination">`; 129 if (total_pages > 1) { 130 h += `<button class="page-btn" data-apipage="${Math.max(1,page-1)}" ${page===1?'disabled':''}>ä¸ä¸é¡µ</button>`; 131 const startP = Math.max(1, Math.min(page-3, total_pages-6)); 132 const endP = Math.min(total_pages, startP+6); 133 for (let i = startP; i <= endP; i++) { 134 h += `<button class="page-btn${i===page?' active':''}" data-apipage="${i}">${i}</button>`; 135 } 136 h += `<button class="page-btn" data-apipage="${Math.min(total_pages,page+1)}" ${page>=total_pages?'disabled':''}>ä¸ä¸é¡µ</button>`; 137 } 138 h += `<span class="page-info">第 ${page} / ${total_pages||1} 页 · ${total.toLocaleString()} bots</span>`; 139 h += `</div>`; 140 141 c.innerHTML = h; 142 143 // Event listeners 144 c.querySelectorAll('[data-range]').forEach(btn => { 145 btn.addEventListener('click', () => { 146 apiBotsState.dateRange = btn.dataset.range; 147 apiBotsState.page = 1; 148 if (btn.dataset.range !== 'custom') { loadTagsFromAPI().then(() => loadBotsFromAPI()); } 149 else { renderApiExplorer(); } 150 }); 151 }); 152 const applyBtn = document.getElementById('apiDateApply'); 153 if (applyBtn) { 154 applyBtn.addEventListener('click', () => { 155 apiBotsState.date_from = document.getElementById('apiDateFrom').value; 156 apiBotsState.date_to = document.getElementById('apiDateTo').value; 157 apiBotsState.page = 1; 158 loadTagsFromAPI().then(() => loadBotsFromAPI()); 159 }); 160 } 161 document.getElementById('apiTagFilter').addEventListener('change', e => { 162 apiBotsState.tag = e.target.value; apiBotsState.page = 1; loadBotsFromAPI(); 163 }); 164 document.getElementById('apiSortBy').addEventListener('change', e => { 165 apiBotsState.sort_by = e.target.value; apiBotsState.page = 1; loadBotsFromAPI(); 166 }); 167 document.getElementById('apiSortOrder').addEventListener('change', e => { 168 apiBotsState.sort_order = e.target.value; apiBotsState.page = 1; loadBotsFromAPI(); 169 }); 170 let searchTimer; 171 document.getElementById('apiSearch').addEventListener('input', e => { 172 clearTimeout(searchTimer); 173 searchTimer = setTimeout(() => { 174 apiBotsState.search = e.target.value; apiBotsState.page = 1; loadBotsFromAPI(); 175 }, 400); 176 }); 177 c.querySelectorAll('th.sortable').forEach(th => { 178 th.addEventListener('click', () => {
179 const k = th.dataset.sortkey; 180 if (!k) return; 181 if (apiBotsState.sort_by === k) apiBotsState.sort_order = apiBotsState.sort_order === 'desc' ? 'asc' : 'desc'; 182 else { apiBotsState.sort_by = k; apiBotsState.sort_order = 'desc'; } 183 apiBotsState.page = 1; loadBotsFromAPI(); 184 }); 185 }); 186 c.querySelectorAll('[data-apipage]').forEach(btn => { 187 btn.addEventListener('click', () => { apiBotsState.page = parseInt(btn.dataset.apipage); loadBotsFromAPI(); }); 188 }); 189 c.querySelectorAll('.clickable-row').forEach(row => { 190 row.addEventListener('click', () => { 191 const botID = row.dataset.botid; 192 const bot = bots.find(b => b.prompt_id === botID); 193 if (bot) { renderApiDetail(bot); switchViewRaw('botdetail'); } 194 }); 195 }); 196} 197 198// Keep old renderExplorer as fallback for gene-annotated data 199function renderExplorer() { 200 // Mark as loaded to prevent activateLibraryTab() from firing a duplicate call 201 window._botsLibraryLoaded = true; 202 loadTagsFromAPI().then(() => loadBotsFromAPI()); 203} 204 205// ââââââââââââââââââââââââââââââââââââââ 206// API Bot Detail 207// ââââââââââââââââââââââââââââââââââââââ 208function _renderMetricCards(b) { 209 // Use pre-computed API values when available, fall back to raw calculation 210 const fs = b.feed_shows || b.home_shows_sum || 0; 211 const fc = b.feed_clicks || b.home_clicks_sum || 0; 212 const tc = b.chats_sum || 0; 213 const cuv = b.chat_users || 0; 214 215 const feedCTR = b.feed_ctr != null ? (b.feed_ctr * 100).toFixed(2) + '%' 216 : fs > 0 ? (fc / fs * 100).toFixed(2) + '%' : 'â'; 217 // chat_per_imp = total chats / feed shows (matches backend metrics.py) 218 const chatPerImp = b.chat_per_imp != null ? b.chat_per_imp 219 : fs > 0 ? tc / fs : null; 220 const chatPerImpStr = chatPerImp !== null ? chatPerImp.toFixed(2) : 'â'; 221 const feedOpenRate = b.feed_open_rate != null ? (b.feed_open_rate * 100).toFixed(2) + '%' 222 : 'â'; 223 const depth = b.depth != null ? Number(b.depth).toFixed(1) 224 : cuv > 0 ? (tc / cuv).toFixed(1) : 'â'; 225 const ret1 = b.retention1 != null ? pct(b.retention1) : 'â'; 226 const ret7 = b.retention7 != null ? pct(b.retention7) : 'â'; 227 228 return ` 229 <div class="metric-card"><div class="metric-label">Feedæå </div><div class="metric-value">${fs.toLocaleString()}</div></div> 230 <div class="metric-card"><div class="metric-label">Feedç¹å»</div><div class="metric-value">${fc.toLocaleString()}</div></div> 231 <div class="metric-card"><div class="metric-label">æ»Chat</div><div class="metric-value">${tc.toLocaleString()}</div></div> 232 <div class="metric-card"><div class="metric-label">FeedCTR</div><div class="metric-value">${feedCTR}</div></div> 233 <div class="metric-card"><div class="metric-label">Chat/Imp</div><div class="metric-value" style="color:#3a86ff">${chatPerImpStr}</div></div> 234 <div class="metric-card"><div class="metric-label">Feedå¼èç</div><div class="metric-value">${feedOpenRate}</div></div> 235 <div class="metric-card"><div class="metric-label">äººåæ·±åº¦</div><div class="metric-value">${depth}</div></div> 236 <div class="metric-card"><div class="metric-label">è天UV</div><div class="metric-value">${cuv.toLocaleString()}</div></div> 237 <div class="metric-card"><div class="metric-label">次æ¥çå</div><div class="metric-value">${ret1}</div></div> 238 <div class="metric-card"><div class="metric-label">7æ¥çå</div><div class="metric-value">${ret7}</div></div>`; 239} 240 241function switchViewRaw(name) { 242 document.querySelectorAll('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.view===name)); 243 document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id==='view-'+name)); 244} 245 246async function renderApiDetail(bot) { 247 const c = document.getElementById('view-botdetail'); 248 const { date_from, date_to } = apiDateRange(); 249 250 // Show initial layout immediately 251 const thumb = bot.thumbnail_url ? `<img src="${bot.thumbnail_url}" style="width:72px;height:72px;border-radius:12px;object-fit:cover;flex-shrink:0;background:#2a2a28;" onerror="this.style.display='none'">` : ''; 252 let h = `<div class="back-area"><button class="btn" id="backToApiExplorer">â è¿åå表</button></div>`; 253 h += `<div class="detail-header" style="display:flex;gap:20px;align-items:flex-start;">${thumb} 254 <div><div class="detail-title">${bot.title||bot.prompt_id}</div> 255 <div style="color:rgba(255,255,255,0.3);font-size:11px;margin-bottom:8px">${bot.v8_primary_tag||bot.tag||''} · ${bot.language||''}</div> 256 <div class="detail-desc" id="apiDetailDesc">Loading content...</div></div></div>`; 257 258 // Render metric cards with placeholder â will be updated when fresh API data arrives 259 h += `<div class="metric-grid" id="apiDetailMetrics">`; 260 h += _renderMetricCards(bot); 261 h += `</div>`; 262 263 h += `<div class="section"><div class="section-header"><div class="section-title">æ¥è¶å¿ Daily Trend</div></div> 264 <div id="apiDailyChart" class="card" style="min-height:60px">Loading...</div></div>`; 265 266 c.innerHTML = h; 267 268 document.getElementById('backToApiExplorer').addEventListener('click', () => { 269 switchViewRaw('bots'); 270 }); 271 272 // Load content and daily data in parallel 273 Promise.all([ 274 fetch(`${API_BASE}/api/bots/${bot.prompt_id}/content`).then(r =>
274 r.ok ? r.json() : null).catch(()=>null), 275 fetch(`${API_BASE}/api/bots/${bot.prompt_id}?date_from=${date_from}&date_to=${date_to}`).then(r => r.ok ? r.json() : null).catch(()=>null), 276 ]).then(([content, detail]) => { 277 const descEl = document.getElementById('apiDetailDesc'); 278 if (descEl && content) { 279 descEl.textContent = content.description || '(no description)'; 280 } else if (descEl) { 281 descEl.textContent = '(content unavailable)'; 282 } 283 284 // Refresh metric cards with precise BITMAP data from detail API 285 const metricsEl = document.getElementById('apiDetailMetrics'); 286 if (metricsEl && detail && detail.bot) { 287 metricsEl.innerHTML = _renderMetricCards(detail.bot); 288 } 289 290 const chartEl = document.getElementById('apiDailyChart'); 291 if (chartEl && detail && detail.daily && detail.daily.length) { 292 const daily = detail.daily; 293 const maxShows = Math.max(...daily.map(d=>d.shows_sum||0), 1); 294 let ch = `<div style="display:flex;align-items:flex-end;gap:6px;height:80px;padding:8px 0">`; 295 daily.forEach(d => { 296 const h_pct = Math.round((d.shows_sum||0)/maxShows*100); 297 const tip = `${d.event_date}: ${(d.shows_sum||0).toLocaleString()} shows`; 298 ch += `<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:3px"> 299 <div style="width:100%;background:#38bdf8;border-radius:2px 2px 0 0;height:${h_pct}%;min-height:2px;opacity:0.7" title="${tip}"></div> 300 <div style="font-size:9px;color:rgba(255,255,255,0.25);writing-mode:vertical-rl;transform:rotate(180deg)">${d.event_date.slice(5)}</div> 301 </div>`; 302 }); 303 ch += `</div>`; 304 ch += `<div style="font-size:10px;color:rgba(255,255,255,0.3);margin-top:2px">æ±é«=Feedæå </div>`; 305 ch += `<div style="display:flex;gap:16px;margin-top:4px;flex-wrap:wrap">`; 306 daily.forEach(d => { 307 const ci = d.chat_imp != null ? fmt(d.chat_imp) : 'â'; 308 ch += `<span style="font-size:11px;color:rgba(255,255,255,0.4)">${d.event_date.slice(5)} CI:<span style="color:rgba(255,255,255,0.6)">${ci}</span></span>`; 309 }); 310 ch += `</div>`; 311 chartEl.innerHTML = ch; 312 } else if (chartEl) { 313 chartEl.textContent = 'No daily data available.'; 314 } 315 }); 316} 317 318// ââââââââââââââââââââââââââââââââââââââ 319// Bot Detail (legacy gene-annotated) 320// ââââââââââââââââââââââââââââââââââââââ 321function renderDetail(bot) { 322 const c = document.getElementById('view-botdetail'); 323 let h = ''; 324 325 h += `<div class="back-area"><button class="btn" id="backToExplorer">\u2190 è¿åå表</button></div>`; 326 327 h += `<div class="detail-header" style="display:flex;gap:20px;align-items:flex-start;"> 328 ${bot.avatar?`<img src="${bot.avatar}" style="width:72px;height:72px;border-radius:12px;object-fit:cover;flex-shrink:0;background:#2a2a28;" onerror="this.style.display='none'">`:''} 329 <div> 330 <div class="detail-title">${bot.title}</div> 331 <div style="margin-bottom:8px">${formatBadge(getFormat(bot))}</div> 332 <div class="detail-desc">${bot.description||'No description available.'}</div> 333 ${bot.link?`<a class="detail-link" href="${bot.link}" target="_blank">Open on Emochi \u2197</a>`:''} 334 </div> 335 </div>`; 336 337 // Metrics 338 h += `<div class="metric-grid"> 339 <div class="metric-card"><div class="metric-label">Chat / Imp</div><div class="metric-value" style="color:#3a86ff">${fmt(bot.chat_imp)}</div></div> 340 <div class="metric-card"><div class="metric-label">Depth</div><div class="metric-value">${fmt(bot.depth,1)}</div></div> 341 <div class="metric-card"><div class="metric-label">Time (min)</div><div class="metric-value">${fmt(bot.time_min,1)}</div></div> 342 <div class="metric-card"><div class="metric-label">CTR</div><div class="metric-value">${pct(bot.ctr)}</div></div> 343 <div class="metric-card"><div class="metric-label">å¼èç</div><div class="metric-value">${pct(bot.open_rate)}</div></div> 344 <div class="metric-card"><div class="metric-label">æå é</div><div class="metric-value">
344${bot.exposure?Math.round(bot.exposure).toLocaleString():'\u2014'}</div></div> 345 <div class="metric-card"><div class="metric-label">èå¤©ç¨æ·</div><div class="metric-value">${bot.chat_users?Math.round(bot.chat_users).toLocaleString():'\u2014'}</div></div> 346 <div class="metric-card"><div class="metric-label">次æ¥çå</div><div class="metric-value">${pct(bot.d1_retention)}</div></div> 347 <div class="metric-card"><div class="metric-label">7æ¥çå</div><div class="metric-value">${pct(bot.d7_retention)}</div></div> 348 </div>`; 349 350 // Format Badge (large) 351 const fmtKey = getFormat(bot); 352 const fmtInfo = FORMAT_LABELS[fmtKey]; 353 if (fmtInfo) { 354 h += `<div class="section"> 355 <div class="section-header"><div class="section-title">å 容形æ</div></div> 356 <div class="card" style="display:flex;align-items:center;gap:16px;"> 357 <span class="format-badge" style="color:${fmtInfo.color};border-color:${fmtInfo.color}30;background:${fmtInfo.color}10;height:32px;font-size:14px;padding:0 16px;">${fmtInfo.name}</span> 358 <span style="font-size:13px;color:rgba(255,255,255,0.4);">${fmtInfo.desc}</span> 359 </div> 360 </div>`; 361 } 362 363 // Gene Profile 364 h += `<div class="section"> 365 <div class="section-header"><div class="section-title">åºå æ¦è§ Gene Profile</div></div> 366 <div class="card"><div class="gene-profile">`; 367 368 const geneItems = [ 369 {dim:'èµ·å§äºä»¶',prefix:'I',val:bot.inciting_incident,sec:bot.inciting_incident_secondary}, 370 {dim:'ä¸ç设å®',prefix:'W',val:bot.setting}, 371 {dim:'Setting IP',prefix:null,val:bot.setting_ip}, 372 {dim:'å°éç¨åº¦',prefix:null,val:bot.setting_confinement}, 373 {dim:'è§è²è§æ¨¡',prefix:null,val:bot.cast_size}, 374 {dim:'è§è²å ³ç³»',prefix:'R',val:bot.cast_relationship}, 375 {dim:'è§è²åºå',prefix:null,val:bot.cast_differentiation}, 376 {dim:'ç¨æ·èº«ä»½',prefix:'U',val:bot.user_identity}, 377 {dim:'ç¨æ·æå',prefix:null,val:bot.user_power}, 378 {dim:'æ ææ¿è¯º',prefix:'E',val:bot.emotional_promise,sec:bot.emotional_promise_secondary}, 379 {dim:'è¯è¨',prefix:null,val:bot.language}, 380 ]; 381 382 for (const g of geneItems) { 383 const primary = g.prefix&&g.val ? pillFull(g.val,g.prefix) : (g.val||'\u2014'); 384 const secondary = g.sec&&g.prefix ? ' + '+pillFull(g.sec,g.prefix) : (g.sec?' + '+g.sec:''); 385 h += `<div class="gene-row"> 386 <div class="gene-dim-label" ${g.prefix?`style="color:${DIM_COLORS[g.prefix]}"`:''} >${g.dim}</div> 387 <div class="gene-dim-value">${primary}${secondary}</div> 388 </div>`; 389 } 390 h += `</div></div></div>`; 391 392 // Cast Archetypes 393 if (bot.cast_archetypes&&bot.cast_archetypes.length) { 394 h += `<div class="section"> 395 <div class="section-header"> 396 <div class="section-accent" style="background:${DIM_COLORS.C}"></div> 397 <div class="section-title">è§è²åå Cast</div> 398 </div> 399 <div class="cast-grid">`; 400 for (const ca of bot.cast_archetypes) { 401 h += `<div class="cast-item"> 402 <span class="cast-name">${ca.name||'?'}</span> 403 ${pillFull(ca.archetype,'C')} 404 </div>`; 405 } 406 h += `</div></div>`; 407 } 408 409 // Tension Mechanisms 410 if (bot.tension_mechanisms&&bot.tension_mechanisms.length) { 411 h += `<div class="section"> 412 <div class="section-header"> 413 <div class="section-accent" style="background:${DIM_COLORS.T}"></div> 414 <div class="section-title">å¼ åæºå¶ Tensions</div> 415 </div> 416 <div style="display:flex;flex-wrap:wrap;gap:6px;">`; 417 for (const t of bot.tension_mechanisms) { 418 h += pillFull(t,'T'); 419 } 420 h += `</div></div>`; 421 } 422 423 // Welcome Message 424 h += `<div class="section"> 425 <div class="section-header"><div class="section-title">Welcome Message</div></div> 426 <div class="card"><pre class="prompt-text">${(bot.welcomeMessage||'N/A').replace(/</g,'<').replace(/>/g,'>')}</pre></div> 427 </div>`; 428 429 // System Prompt (Collapsible) 430 h += `<div class="section"> 431 <div class="section-header"><div class="section-title">System Prompt</div></div> 432 <button class="collapsible-toggle" id="sysPromptToggle">ç¹å»å±å¼ System Prompt</button> 433 <div class="collapsible-content" id="sysPromptContent"> 434 <div class="card" style="margin-top:8px;"><pre class="prompt-text">${(bot.initPrompt||'N/A').replace(/</g,'<').replace(/>/g,'>')}</pre></div> 435 </div> 436 </div>`; 437 438 c.innerHTML = h; 439 document.getElementById('backToExplorer').addEventListener('click',()=>switchView('bots')); 440 441 // Collapsible toggle 442 document.getElementById('sysPromptToggle').addEventListener('click', function() { 443 const content = document.getElementById('sysPromptContent'); 444 const isOpen = content.classList.contains('open'); 445 content.classList.toggle('open'); 446 this.textContent = isOpen ? 'ç¹å»å±å¼ System Prompt' : 'ç¹å»æ¶èµ· System Prompt'; 447 }); 448}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.