New /onboard page. Client-facing wizard for getting real data into the system without engineering help. Flow: 1. Drop a CSV (or click 'Use the sample as my data' — ships a 25-row realistic staffing roster under /samples/staffing_roster_sample.csv) 2. Browser parses client-side. Columns auto-typed (text/int/decimal/ date). PII flagged by name hint AND content regex (emails, phones). First rows previewed. Read-only — nothing written yet. 3. Name the dataset (lowercase+underscores). Commit. 4. Post-commit: dataset is live. Shows 4 next steps the operator can take (SQL query, vector index, dashboard search, playbook training). Backend: - /onboard serves onboard.html - /samples/*.csv serves CSV files from mcp-server/samples/ with filename validation (only [a-zA-Z0-9_-.]+.csv, prevents path traversal) - /onboard/ingest forwards multipart/form-data to gateway /ingest/file preserving the boundary. The generic /api/* passthrough breaks multipart because it reads as text and forwards as JSON; this route uses arrayBuffer + original Content-Type. Verified end-to-end: upload sample roster (25 rows, 12 columns) → parse in browser → show columns + PII flags + preview → commit → gateway writes Parquet, registers in catalog → immediately queryable: SELECT * FROM onboard_demo2 LIMIT 3 → Sarah Johnson, Forklift Operator, Chicago, IL, 0.92 Round-trip <1 second. Nav updated on all pages to link Onboard. Shipped with a sample CSV so the full flow is demonstrable without real client data. When a real client shows up, same path — they upload their CSV. No engineering ticket, no code change, no schema pre-definition. Security: sample filename regex prevents path traversal. CSV parse is client-side pure JS (no DOM injection). Commit uses existing /ingest/file validation (schema fingerprint, PII server-side, content-hash dedup).
469 lines
26 KiB
HTML
469 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en"><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Lakehouse — What Your Staffing System Would Do</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
|
||
a{color:#58a6ff;text-decoration:none}
|
||
a:hover{color:#79c0ff}
|
||
|
||
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:10}
|
||
.bar h1{font-size:14px;font-weight:600;color:#e6edf3;letter-spacing:-0.2px}
|
||
.bar nav{display:flex;gap:2px}
|
||
.bar nav a{font-size:12px;color:#545d68;padding:6px 14px;border-radius:6px;transition:all 0.15s}
|
||
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
||
.bar nav a.active{color:#e6edf3;background:#1c2333}
|
||
.bar .rt{font-size:11px;color:#545d68}
|
||
|
||
.wrap{max-width:1040px;margin:0 auto;padding:28px 20px 60px}
|
||
|
||
.chapter{margin-bottom:48px}
|
||
.chapter .num{color:#545d68;font-size:11px;font-weight:600;letter-spacing:1.6px;text-transform:uppercase;margin-bottom:6px}
|
||
.chapter h2{color:#e6edf3;font-size:24px;font-weight:700;letter-spacing:-0.4px;margin-bottom:8px;line-height:1.2}
|
||
.chapter .lede{color:#8b949e;font-size:14px;margin-bottom:18px;max-width:680px;line-height:1.6}
|
||
|
||
.card{background:#0d1117;border:1px solid #171d27;border-radius:12px;padding:20px;margin-bottom:12px}
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px}
|
||
|
||
.stat-lg{padding:18px 20px}
|
||
.stat-lg .n{font-size:30px;font-weight:800;color:#e6edf3;letter-spacing:-1px;line-height:1}
|
||
.stat-lg .l{font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:8px;font-weight:600}
|
||
.stat-lg .sub{font-size:12px;color:#8b949e;margin-top:4px}
|
||
|
||
.pill{display:inline-block;padding:3px 10px;border-radius:10px;font-size:10px;font-weight:600;letter-spacing:0.3px;margin-right:6px}
|
||
.pill.real{background:#0d2818;color:#3fb950;border:1px solid #2ea04380}
|
||
.pill.synth{background:#1a1a2e;color:#bc8cff;border:1px solid #5530988c}
|
||
.pill.flow{background:#0d2340;color:#58a6ff;border:1px solid #1f6feb80}
|
||
|
||
.row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:#0d1117;border:1px solid #171d27;border-radius:8px;margin-bottom:6px;font-size:13px}
|
||
.row:hover{border-color:#21262d}
|
||
.row .title{color:#e6edf3;font-weight:500}
|
||
.row .meta{color:#8b949e;font-size:11px;margin-top:2px}
|
||
.row .val{color:#58a6ff;font-weight:600;white-space:nowrap}
|
||
|
||
details{background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 14px;margin-bottom:6px}
|
||
details summary{cursor:pointer;font-size:13px;color:#e6edf3;font-weight:500;list-style:none}
|
||
details summary::-webkit-details-marker{display:none}
|
||
details .body{padding-top:10px;font-size:12px;color:#8b949e}
|
||
|
||
.accent-l{border-left:3px solid #2ea043}
|
||
.accent-b{border-left:3px solid #1f6feb}
|
||
.accent-a{border-left:3px solid #bc8cff}
|
||
.accent-w{border-left:3px solid #d29922}
|
||
|
||
.worker{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px}
|
||
.worker .av{width:28px;height:28px;border-radius:6px;background:#1a2744;display:flex;align-items:center;justify-content:center;font-weight:600;color:#e6edf3;font-size:10px;flex-shrink:0}
|
||
.worker .info{flex:1;min-width:0}
|
||
.worker .nm{color:#e6edf3;font-weight:500}
|
||
.worker .why{color:#545d68;font-size:11px;margin-top:1px}
|
||
.worker .score{color:#58a6ff;font-size:11px;font-weight:600;white-space:nowrap}
|
||
|
||
.mem-chip{background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;font-size:11px;color:#86efac;line-height:1.5;margin-top:6px}
|
||
.mem-chip .l{color:#3fb950;font-weight:600;margin-right:6px}
|
||
|
||
.boost-chip{display:inline-block;margin-left:6px;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#0d2818;border:1px solid #2ea043;color:#3fb950;vertical-align:middle}
|
||
|
||
.try-box{background:#0d1117;border:1px solid #171d27;border-radius:12px;padding:16px}
|
||
.try-box input{width:100%;padding:12px 16px;background:#161b22;border:1px solid #21262d;border-radius:8px;color:#e6edf3;font-size:13px;outline:none;margin-bottom:10px}
|
||
.try-box input:focus{border-color:#388bfd}
|
||
.try-box button{padding:10px 20px;background:#1f6feb;border:none;border-radius:8px;color:#fff;font-size:13px;font-weight:600;cursor:pointer}
|
||
.try-box button:hover{background:#388bfd}
|
||
.try-box button:disabled{opacity:0.5;cursor:wait}
|
||
|
||
.footer{border-top:1px solid #171d27;padding:20px;text-align:center;color:#3d444d;font-size:11px}
|
||
|
||
.loading{color:#484f58;font-style:italic;padding:20px 0;text-align:center}
|
||
.err{color:#f85149;font-size:12px;padding:10px}
|
||
|
||
.narr{color:#8b949e;font-size:13px;line-height:1.7;margin:10px 0;padding:10px 14px;border-left:2px solid #21262d}
|
||
.narr strong{color:#c9d1d9;font-weight:600}
|
||
|
||
.step-label{color:#58a6ff;font-size:12px;margin-top:12px;font-weight:600}
|
||
.step-body{color:#c9d1d9;font-size:13px;margin-top:2px}
|
||
|
||
@media(max-width:720px){
|
||
.wrap{padding:20px 12px 40px}
|
||
.chapter h2{font-size:20px}
|
||
.bar nav{display:none}
|
||
}
|
||
</style></head>
|
||
<body>
|
||
|
||
<div class="bar">
|
||
<h1>Lakehouse — What Your Staffing System Would Do</h1>
|
||
<nav>
|
||
<a href=".">Dashboard</a>
|
||
<a href="console" class="active">Walkthrough</a>
|
||
<a href="proof">Architecture</a>
|
||
<a href="spec">Spec</a>
|
||
<a href="onboard">Onboard</a>
|
||
</nav>
|
||
<div class="rt" id="hdr-time">Reading live state…</div>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 1</div>
|
||
<h2>Right now, this system is already thinking</h2>
|
||
<div class="lede">Before you touched anything, it pulled real Chicago building-permit data, measured demand, checked your bench, and began flagging roles that need attention. This isn't theoretical — open your browser network tab and watch the fetches land.</div>
|
||
<div class="grid" id="ch1-stats"><div class="loading">Fetching live state…</div></div>
|
||
<div class="narr" id="ch1-narr"></div>
|
||
</div>
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 2</div>
|
||
<h2>The demand signal is real, not made up</h2>
|
||
<div class="lede">Chicago's Department of Buildings publishes every permit they issue. Below are the largest categories of construction filed in the last 30 days. If a staffer doesn't believe our numbers, they can verify at <a href="https://data.cityofchicago.org/Buildings/Building-Permits/ydr8-5enu" target="_blank" rel="noopener">data.cityofchicago.org</a>.</div>
|
||
<div id="ch2-permits"><div class="loading">Loading permit feed…</div></div>
|
||
</div>
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 3</div>
|
||
<h2>Where your own data would live</h2>
|
||
<div class="lede">The system stores data in labeled catalogs. Purple pills = synthetic stand-ins you'd swap for your real ATS/CRM/call-log exports. Blue pills = data the system generates about itself (playbooks, audit trails). Nothing else in the pipeline changes — only the source.</div>
|
||
<div id="ch3-datasets"><div class="loading">Enumerating catalog…</div></div>
|
||
<div class="narr">
|
||
<strong>The swap path.</strong> workers_500k → your ATS export (same schema shape). candidates → your CRM. call_log → your phone system's CDR. timesheets → your payroll export. Once ingested, every behavior you see on the dashboard applies to your real data. No re-training. No replatform.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 4</div>
|
||
<h2>Watch the system rank candidates in real time</h2>
|
||
<div class="lede">This takes the most recent Chicago permit, derives the staffing need, pulls ranked candidates from the 500K bench, and shows you why each one ranked. Everything below loaded in about 3 seconds against the live system.</div>
|
||
<div id="ch4-demo"><div class="loading">Running demo query…</div></div>
|
||
</div>
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 5</div>
|
||
<h2>Every action compounds — the CRM-killer</h2>
|
||
<div class="lede">A CRM stores. This system compounds. Every successful fill, every no-show, every phone call becomes a re-ranking signal on the next query. Below is the live playbook memory state. The number grows as the app gets used.</div>
|
||
<div id="ch5-memory"><div class="loading">Reading playbook memory…</div></div>
|
||
</div>
|
||
|
||
<div class="chapter">
|
||
<div class="num">Chapter 6</div>
|
||
<h2>Try it yourself</h2>
|
||
<div class="lede">Type any staffing question. The system picks the right search path (smart-parse, semantic discovery, analytics), shows what it understood, and returns ranked results with memory signal.</div>
|
||
<div class="try-box">
|
||
<input type="text" id="try-q" placeholder="e.g. reliable forklift operators in Chicago with OSHA certs" onkeydown="if(event.key==='Enter')runTry()">
|
||
<button id="try-btn" onclick="runTry()">Ask</button>
|
||
<div id="try-out" style="margin-top:16px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="footer">Lakehouse · demo instance · <a href="proof">architecture & benchmarks</a></div>
|
||
|
||
<script>
|
||
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||
var A=location.origin+P;
|
||
|
||
// DOM helpers — all dynamic content goes through these. No innerHTML
|
||
// anywhere in the script; every API-derived string passes through
|
||
// textContent so no injection path regardless of upstream data.
|
||
function el(tag, cls, text){
|
||
var e=document.createElement(tag);
|
||
if(cls) e.className=cls;
|
||
if(text!==undefined && text!==null) e.textContent=String(text);
|
||
return e;
|
||
}
|
||
function setAttrs(e, obj){
|
||
for(var k in obj) if(Object.prototype.hasOwnProperty.call(obj,k)) e.setAttribute(k,obj[k]);
|
||
return e;
|
||
}
|
||
|
||
function api(path, body){
|
||
return fetch(A+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(function(r){return r.json()});
|
||
}
|
||
function apiGet(path){
|
||
return fetch(A+path).then(function(r){return r.json()});
|
||
}
|
||
|
||
window.addEventListener('load',function(){
|
||
loadChapter1();
|
||
loadChapter2();
|
||
loadChapter3();
|
||
loadChapter4();
|
||
loadChapter5();
|
||
});
|
||
|
||
// ─── Chapter 1 ────────────────────────────────────────────
|
||
function loadChapter1(){
|
||
Promise.all([
|
||
api('/intelligence/staffing_forecast',{}).catch(function(){return {}}),
|
||
apiGet('/api/vectors/playbook_memory/stats').catch(function(){return {}}),
|
||
apiGet('/api/catalog/datasets').catch(function(){return []}),
|
||
]).then(function(all){
|
||
var f=all[0]||{}, mem=all[1]||{}, ds=all[2]||[];
|
||
var totalRows=0;
|
||
if(Array.isArray(ds)) ds.forEach(function(d){totalRows+=(d.row_count||0)});
|
||
var host=document.getElementById('ch1-stats');host.textContent='';
|
||
addStat(host,'$'+(f.total_cost||0).toLocaleString('en-US',{maximumFractionDigits:0}),'Construction pipeline',(f.permit_count||0)+' permits last 30 days','accent-b');
|
||
addStat(host,(f.total_estimated_workers||0).toLocaleString(),'Predicted worker demand','split across '+((f.forecast||[]).length)+' roles','accent-w');
|
||
addStat(host,(totalRows||0).toLocaleString(),'Rows under management',(Array.isArray(ds)?ds.length:0)+' datasets · real + synthetic','accent-a');
|
||
addStat(host,(mem.entries||0).toLocaleString(),'Playbooks remembered',(mem.total_names_endorsed||0)+' endorsed worker-tags','accent-l');
|
||
|
||
var narrEl=document.getElementById('ch1-narr');
|
||
narrEl.textContent='';
|
||
var critical=(f.critical_roles||0), tight=(f.tight_roles||0);
|
||
if(critical>0){
|
||
narrEl.appendChild(document.createTextNode('The system has already flagged '));
|
||
narrEl.appendChild(el('strong',null,critical+' role'+(critical!==1?'s':'')+' at critical coverage'));
|
||
narrEl.appendChild(document.createTextNode(' — supply is below demand. A CRM would surface this only if someone ran the report. This system surfaces it before you log in.'));
|
||
} else if(tight>0){
|
||
narrEl.appendChild(document.createTextNode('The system flagged '));
|
||
narrEl.appendChild(el('strong',null,tight+' role'+(tight!==1?'s':'')+' as tight'));
|
||
narrEl.appendChild(document.createTextNode(' — watch for supply compression. Pre-emptive signal, no query required.'));
|
||
} else {
|
||
narrEl.textContent='No coverage gaps right now — bench exceeds predicted demand across all roles. The system is tracking this continuously; check back tomorrow and the numbers will have moved.';
|
||
}
|
||
}).catch(function(e){
|
||
var h=document.getElementById('ch1-stats');h.textContent='';h.appendChild(el('div','err','Live state unavailable: '+(e.message||e)));
|
||
});
|
||
document.getElementById('hdr-time').textContent='Snapshot · '+new Date().toLocaleTimeString();
|
||
}
|
||
function addStat(host,n,l,sub,cls){
|
||
var d=el('div','card stat-lg '+(cls||''));
|
||
d.appendChild(el('div','n',n));
|
||
d.appendChild(el('div','l',l));
|
||
d.appendChild(el('div','sub',sub||''));
|
||
host.appendChild(d);
|
||
}
|
||
|
||
// ─── Chapter 2 ────────────────────────────────────────────
|
||
function loadChapter2(){
|
||
api('/intelligence/market',{}).then(function(r){
|
||
var host=document.getElementById('ch2-permits');host.textContent='';
|
||
if(r.error){host.appendChild(el('div','err','Permit feed error: '+r.error));return}
|
||
var byType=r.by_type||[];
|
||
if(byType.length===0){host.appendChild(el('div','narr','No permits returned by the Chicago API right now.'));return}
|
||
byType.slice(0,8).forEach(function(t){
|
||
var det=document.createElement('details');
|
||
var summary=document.createElement('summary');
|
||
summary.appendChild(el('span','pill real','LIVE · DATA.CITYOFCHICAGO.ORG'));
|
||
summary.appendChild(document.createTextNode(' '));
|
||
var wt=el('strong',null,t.work_type||'General construction');
|
||
summary.appendChild(wt);
|
||
summary.appendChild(document.createTextNode(' · '+(t.permits||0)+' permits · $'+(t.total_cost||0).toLocaleString('en-US',{maximumFractionDigits:0})+' · est '+(t.estimated_workers||0)+' workers needed'));
|
||
det.appendChild(summary);
|
||
var body=el('div','body');
|
||
body.appendChild(document.createTextNode('Likely roles: '+((t.needed_roles||[]).join(', ')||'—')+'. '));
|
||
body.appendChild(document.createTextNode('At industry heuristic (~$150K per worker), this work type alone implies '));
|
||
body.appendChild(el('strong',null,(t.estimated_workers||0)+' staffing opportunities'));
|
||
body.appendChild(document.createTextNode(' over the next 30-60 days.'));
|
||
det.appendChild(body);
|
||
host.appendChild(det);
|
||
});
|
||
}).catch(function(e){
|
||
var h=document.getElementById('ch2-permits');h.textContent='';h.appendChild(el('div','err',e.message||String(e)));
|
||
});
|
||
}
|
||
|
||
// ─── Chapter 3 ────────────────────────────────────────────
|
||
function loadChapter3(){
|
||
apiGet('/api/catalog/datasets').then(function(ds){
|
||
var host=document.getElementById('ch3-datasets');host.textContent='';
|
||
if(!Array.isArray(ds)){host.appendChild(el('div','err','No datasets returned.'));return}
|
||
var bigOnes=ds.filter(function(d){return (d.row_count||0)>=500}).sort(function(a,b){return (b.row_count||0)-(a.row_count||0)});
|
||
var playbookKeys=['successful_playbooks','playbook_memory','kb_'];
|
||
bigOnes.slice(0,12).forEach(function(d){
|
||
var row=el('div','row');
|
||
var left=document.createElement('div');left.style.flex='1';left.style.minWidth='0';
|
||
var t=el('div','title');
|
||
var pillCls='synth', pillLabel='SWAP FOR YOUR DATA';
|
||
if(playbookKeys.some(function(k){return (d.name||'').indexOf(k)>=0})){pillCls='flow';pillLabel='SYSTEM-GENERATED'}
|
||
else if((d.name||'').indexOf('threat_')===0){pillCls='flow';pillLabel='SYSTEM-GENERATED'}
|
||
t.appendChild(el('span','pill '+pillCls,pillLabel));
|
||
t.appendChild(document.createTextNode(' '+(d.name||'(unnamed)')));
|
||
var m=el('div','meta',(d.columns?d.columns.length+' columns · ':'')+(d.objects?d.objects.length+' parquet file'+(d.objects.length!==1?'s':''):''));
|
||
left.appendChild(t);left.appendChild(m);
|
||
var v=el('div','val',(d.row_count||0).toLocaleString()+' rows');
|
||
row.appendChild(left);row.appendChild(v);host.appendChild(row);
|
||
});
|
||
}).catch(function(e){
|
||
var h=document.getElementById('ch3-datasets');h.textContent='';h.appendChild(el('div','err','Catalog unavailable: '+(e.message||e)));
|
||
});
|
||
}
|
||
|
||
// ─── Chapter 4 ────────────────────────────────────────────
|
||
function loadChapter4(){
|
||
api('/intelligence/permit_contracts',{}).then(function(r){
|
||
var host=document.getElementById('ch4-demo');host.textContent='';
|
||
if(!r.contracts||r.contracts.length===0){host.appendChild(el('div','loading','No contracts returned.'));return}
|
||
var c=r.contracts[0];
|
||
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
||
var card=el('div','card accent-b');
|
||
var title=el('div',null,'$'+(p.cost||0).toLocaleString()+' · '+(p.work_type||''));
|
||
title.style.cssText='font-size:15px;font-weight:600;color:#e6edf3';
|
||
card.appendChild(title);
|
||
var addr=el('div',null,(p.address||'')+' · Chicago, IL · filed '+(p.issue_date||''));
|
||
addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
||
card.appendChild(addr);
|
||
|
||
card.appendChild(el('div','step-label','STEP 1 · Derive staffing need'));
|
||
var s1=el('div','step-body');
|
||
s1.appendChild(document.createTextNode('Industry heuristic: ~1 worker per $150K of permit cost, capped 2-8. Resulting contract: '));
|
||
s1.appendChild(el('strong',null,(prop.count||0)+'× '+(prop.role||'')));
|
||
s1.appendChild(document.createTextNode(' in Chicago, IL.'));
|
||
card.appendChild(s1);
|
||
|
||
card.appendChild(el('div','step-label','STEP 2 · Narrow via SQL ('+((prop.pool_size||0).toLocaleString())+' candidates match)'));
|
||
card.appendChild(el('div','step-body',"role = '"+(prop.role||'')+"' AND state = 'IL' AND city = 'Chicago' AND CAST(availability AS DOUBLE) > 0.5"));
|
||
|
||
card.appendChild(el('div','step-label','STEP 3 · Rank via semantic match + playbook boost'));
|
||
host.appendChild(card);
|
||
|
||
var list=document.createElement('div');list.style.marginTop='6px';
|
||
(prop.candidates||[]).slice(0,5).forEach(function(cand,i){
|
||
var w=el('div','worker');
|
||
var initials=(cand.name||'?').split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
w.appendChild(el('div','av',initials));
|
||
var info=el('div','info');
|
||
var nm=el('div','nm',cand.name||cand.doc_id||'?');
|
||
if((cand.playbook_boost||0)>0){
|
||
var ncit=(cand.playbook_citations||[]).length;
|
||
nm.appendChild(el('span','boost-chip','Endorsed · '+ncit+' past fill'+(ncit!==1?'s':'')));
|
||
}
|
||
info.appendChild(nm);
|
||
var why=cand.doc_id+' · '+(cand.playbook_boost>0?'boosted +'+cand.playbook_boost.toFixed(3)+' by memory · ':'')+'semantic score '+(cand.score||0).toFixed(3);
|
||
info.appendChild(el('div','why',why));
|
||
w.appendChild(info);
|
||
w.appendChild(el('div','score','#'+(i+1)));
|
||
list.appendChild(w);
|
||
});
|
||
card.appendChild(list);
|
||
|
||
if(c.discovered_pattern||c.pattern_matched>0){
|
||
var mem=el('div','mem-chip');
|
||
mem.appendChild(el('span','l','MEMORY ('+(c.pattern_matched||0)+' past playbooks):'));
|
||
mem.appendChild(document.createTextNode(' '+(c.discovered_pattern||'memory is sparse for this role+geo')));
|
||
card.appendChild(mem);
|
||
}
|
||
|
||
if(tl.days_to_deadline!==undefined){
|
||
var tm=document.createElement('div');
|
||
tm.style.cssText='margin-top:12px;padding-top:10px;border-top:1px solid #171d27;font-size:12px;color:#8b949e';
|
||
tm.appendChild(el('strong',null,'Timeline. '));
|
||
tm.childNodes[0].style && (tm.childNodes[0].style.color='#c9d1d9');
|
||
var dd=tl.days_to_deadline;
|
||
var when=(dd<=0?Math.abs(dd)+' days overdue':dd+' days out');
|
||
tm.appendChild(document.createTextNode('Staffing window opens '+(tl.staffing_window_opens||'')+' ('+when+'). Construction starts '+(tl.estimated_construction_start||'')+'. Urgency: '));
|
||
tm.appendChild(el('strong',null,(tl.urgency||'').toUpperCase()));
|
||
tm.appendChild(document.createTextNode('.'));
|
||
card.appendChild(tm);
|
||
}
|
||
|
||
var note=el('div','narr');
|
||
note.appendChild(el('strong',null,'This is the CRM gap. '));
|
||
note.appendChild(document.createTextNode('A CRM would tell you the permit exists. It would NOT narrow to matching candidates, would NOT boost past-successful workers, would NOT flag patterns across similar past fills, would NOT count down to the deadline. All that happens here, pre-computed, before the staffer clicks anything.'));
|
||
host.appendChild(note);
|
||
}).catch(function(e){
|
||
var h=document.getElementById('ch4-demo');h.textContent='';h.appendChild(el('div','err','Demo failed: '+(e.message||e)));
|
||
});
|
||
}
|
||
|
||
// ─── Chapter 5 ────────────────────────────────────────────
|
||
function loadChapter5(){
|
||
apiGet('/api/vectors/playbook_memory/stats').then(function(mem){
|
||
var host=document.getElementById('ch5-memory');host.textContent='';
|
||
var card=el('div','card accent-l');
|
||
var big=el('div',null,(mem.entries||0).toLocaleString());
|
||
big.style.cssText='font-size:30px;font-weight:800;color:#e6edf3;line-height:1';
|
||
card.appendChild(big);
|
||
var lb=el('div',null,'Playbooks in memory');
|
||
lb.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:8px;font-weight:600';
|
||
card.appendChild(lb);
|
||
var sub=el('div',null,(mem.total_names_endorsed||0).toLocaleString()+' endorsed worker-tags · each one boosts future matching queries');
|
||
sub.style.cssText='font-size:12px;color:#8b949e;margin-top:4px';
|
||
card.appendChild(sub);
|
||
host.appendChild(card);
|
||
|
||
if(mem.sample&&mem.sample.length){
|
||
var hdr=document.createElement('div');
|
||
hdr.style.cssText='color:#545d68;font-size:11px;text-transform:uppercase;letter-spacing:1.4px;font-weight:600;margin:14px 0 8px';
|
||
hdr.textContent='Sample playbooks held in memory right now';
|
||
host.appendChild(hdr);
|
||
mem.sample.slice(0,5).forEach(function(pb){
|
||
var row=el('div','row');
|
||
var left=document.createElement('div');left.style.flex='1';left.style.minWidth='0';
|
||
left.appendChild(el('div','title',pb.operation||'(no operation)'));
|
||
left.appendChild(el('div','meta',(pb.city||'')+', '+(pb.state||'')+' · endorsed: '+((pb.endorsed||[]).slice(0,3).join(', ')||'—')));
|
||
row.appendChild(left);host.appendChild(row);
|
||
});
|
||
}
|
||
|
||
var narr=el('div','narr');
|
||
narr.appendChild(el('strong',null,'What this means for a staffer. '));
|
||
narr.appendChild(document.createTextNode('When you log a successful fill, this counter grows by one. Next time someone searches for a similar role+city, the system finds your past pick and boosts them to the top. Over months, the system becomes a record of who has actually worked out — not just who is on the roster.'));
|
||
host.appendChild(narr);
|
||
}).catch(function(e){
|
||
var h=document.getElementById('ch5-memory');h.textContent='';h.appendChild(el('div','err','Memory unavailable: '+(e.message||e)));
|
||
});
|
||
}
|
||
|
||
// ─── Chapter 6 ────────────────────────────────────────────
|
||
function runTry(){
|
||
var q=document.getElementById('try-q').value.trim();if(!q)return;
|
||
var btn=document.getElementById('try-btn'),out=document.getElementById('try-out');
|
||
btn.disabled=true;btn.textContent='Thinking…';out.textContent='';out.appendChild(el('div','loading','Running…'));
|
||
api('/intelligence/chat',{message:q}).then(function(d){
|
||
btn.disabled=false;btn.textContent='Ask';out.textContent='';
|
||
var card=el('div','card accent-b');
|
||
if(d.understood&&d.understood.length){
|
||
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px';
|
||
d.understood.forEach(function(u){
|
||
var tg=el('span',null,u);
|
||
tg.style.cssText='padding:3px 10px;border-radius:10px;font-size:11px;background:#1a274420;color:#58a6ff;border:1px solid #1a274480';
|
||
tags.appendChild(tg);
|
||
});
|
||
card.appendChild(tags);
|
||
}
|
||
var sum=el('div',null,d.summary||'(no summary)');
|
||
sum.style.cssText='color:#c9d1d9;font-size:13px;margin-bottom:10px';
|
||
card.appendChild(sum);
|
||
|
||
if(d.pattern_playbooks_matched>0&&d.discovered_pattern){
|
||
var mem=el('div','mem-chip');
|
||
mem.appendChild(el('span','l','MEMORY ('+d.pattern_playbooks_matched+' playbooks):'));
|
||
mem.appendChild(document.createTextNode(' '+d.discovered_pattern));
|
||
card.appendChild(mem);
|
||
}
|
||
|
||
var workers=d.sql_results||d.vector_results||d.results||[];
|
||
workers.slice(0,5).forEach(function(w,i){
|
||
var row=el('div','worker');
|
||
var nm=w.name||(w.text||'').split('—')[0].trim()||w.doc_id||'?';
|
||
var initials=nm.split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
|
||
row.appendChild(el('div','av',initials));
|
||
var info=el('div','info');
|
||
var n=el('div','nm',nm);
|
||
if((w.playbook_boost||0)>0){
|
||
n.appendChild(el('span','boost-chip','Endorsed · '+((w.playbook_citations||[]).length||'?')+' past fill(s)'));
|
||
}
|
||
info.appendChild(n);
|
||
var bits=[];
|
||
if(w.role) bits.push(w.role);
|
||
if(w.city&&w.state) bits.push(w.city+', '+w.state);
|
||
if(w.rel!==undefined) bits.push('reliability '+Math.round(w.rel*100)+'%');
|
||
if(w.avail!==undefined) bits.push('availability '+Math.round(w.avail*100)+'%');
|
||
info.appendChild(el('div','why',bits.join(' · ')||'AI semantic match'));
|
||
row.appendChild(info);
|
||
row.appendChild(el('div','score','#'+(i+1)));
|
||
card.appendChild(row);
|
||
});
|
||
var meta=el('div',null,(d.duration_ms||0)+'ms · type='+(d.type||'smart_search'));
|
||
meta.style.cssText='color:#545d68;font-size:11px;margin-top:10px';
|
||
card.appendChild(meta);
|
||
out.appendChild(card);
|
||
}).catch(function(e){
|
||
btn.disabled=false;btn.textContent='Ask';
|
||
out.textContent='';out.appendChild(el('div','err',e.message||String(e)));
|
||
});
|
||
}
|
||
</script>
|
||
</body></html>
|