[object Object]

← back to Commercialrealestate

CRCP root: add working client-side NL chat bar at top + remove Top-brokers people panel on load (show properties)

376087d9e5a2a3b1f11ac2e6983c0b111239558a · 2026-08-18 16:16:03 -0700 · Steve Abrams

Files touched

Diff

commit 376087d9e5a2a3b1f11ac2e6983c0b111239558a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 16:16:03 2026 -0700

    CRCP root: add working client-side NL chat bar at top + remove Top-brokers people panel on load (show properties)
---
 public/index.html | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 85 insertions(+), 6 deletions(-)

diff --git a/public/index.html b/public/index.html
index b18285d..3fb2b6b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -408,12 +408,37 @@
   <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
   <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
   <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
-  <a href="#brokers" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
   <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
   <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
   </div><!-- /.topnav -->
 </header>
 
+<!-- Real natural-language chat bar at the TOP — "4 units in los angeles" → drives filters + re-renders (client-side, no server) -->
+<div id="chatbar" style="padding:9px 18px 10px;border-bottom:1px solid var(--line);background:var(--bg2,var(--card));position:sticky;top:57px;z-index:20;">
+  <div style="display:flex;align-items:center;gap:9px;">
+    <span style="font-size:16px" title="Ask for properties in plain English">💬</span>
+    <input id="chatq" type="text" autocomplete="off" spellcheck="false"
+      placeholder="Ask for what you want — e.g.  “4 units in Los Angeles”  ·  “multifamily under $2M”  ·  “retail 6% cap”  ·  “all cash”  (Enter)"
+      style="flex:1;min-width:0;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:9px 13px;font-size:14px;">
+    <button id="chatgo" class="ex" title="Apply this request">Ask ▸</button>
+    <button id="chatclear" class="ex" title="Clear the chat filters">Clear</button>
+    <span id="chatmsg" style="font-size:12px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:26%"></span>
+  </div>
+  <div id="chatex" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:7px;padding-left:26px;font-size:11px;color:var(--mut);align-items:center;">
+    <span style="opacity:.7">Try:</span>
+    <button class="chatchip" data-q="4 units in Los Angeles">4 units in Los Angeles</button>
+    <button class="chatchip" data-q="unwarrantable condos">unwarrantable condos</button>
+    <button class="chatchip" data-q="multifamily under $2M">multifamily under $2M</button>
+    <button class="chatchip" data-q="retail 6% cap">retail 6% cap</button>
+    <button class="chatchip" data-q="office in Pasadena">office in Pasadena</button>
+    <button class="chatchip" data-q="non-QM condos">non-QM condos</button>
+    <button class="chatchip" data-q="built after 2000">built after 2000</button>
+    <button class="chatchip" data-q="all cash">all cash</button>
+  </div>
+</div>
+<style>.chatchip{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:20px;padding:3px 11px;font-size:11px;cursor:pointer;transition:all .12s;} .chatchip:hover{border-color:var(--blue,#3b82f6);color:var(--blue,#3b82f6);}</style>
+
 <div id="scrim"></div>
 <div class="shell">
 <aside id="drawer">
@@ -529,14 +554,11 @@
     .mls .hist{color:var(--blue);font-size:11px;}
     .phitem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} .phitem .m{color:var(--mut);}
   </style>
-  <section id="brokers">
-    <h2>🎯 Top brokers</h2>
-    <div class="bksub">Brokers ranked by the quality and mix of their <b>current listings</b>. Click a broker to expand their listings (spreadsheet-style) + a <b>Sold</b> button. Each property → <b>100-yr history by address</b> (no owner names). Contact details on the ✉ button.</div>
-    <div id="bkleads"><div class="gate">loading ranked brokers…</div></div>
-  </section>
+  <!-- "Top brokers" people panel removed from the root onload (Steve 2026-08-18: show properties, not people). Broker directory lives at /broker-grid.html. The #bkov modal below is retained — it's shared by the property-grid ✉ contact popup. -->
   <div id="bkov"><div id="bkmodal"><button class="x" aria-label="Close" onclick="document.getElementById('bkov').classList.remove('on')">✕</button><div id="bkmbody"></div></div></div>
   <script>
   (function(){
+    if(!document.getElementById('bkleads')) return;   // "Top brokers" panel removed from root onload — this populate script no-ops (property-grid ✉ uses the global openContact, not this closure).
     const bq=s=>document.querySelector(s);
     const money=n=>n?'$'+Number(n).toLocaleString():'—';
     const esc=s=>(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
@@ -1669,6 +1691,63 @@ function syncChips(){
   $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
 }
 
+// ---- Top-of-page natural-language chat bar (client-side; drives F via applyAction) ----
+const CHAT_TYPES=[['multifamily','Multifamily'],['multi-family','Multifamily'],['multi family','Multifamily'],['apartment','Multifamily'],['office','Office'],['industrial','Industrial'],['retail','Retail'],['mixed-use','Mixed-use'],['mixed use','Mixed-use'],['net lease','Retail'],['net-lease','Retail'],['nnn','Retail'],['land','Land'],['hotel','Hospitality'],['hospitality','Hospitality']];
+function chatMoney(s){ s=String(s).toLowerCase().replace(/[,\s$]/g,''); let n=parseFloat(s); if(isNaN(n))return null; if(/m/.test(s))n*=1e6; else if(/k/.test(s))n*=1e3; return Math.round(n); }
+function chatCities(){ try{ return [...new Set((DATA||[]).map(r=>(r.city||'').trim()).filter(Boolean))].sort((a,b)=>b.length-a.length); }catch(e){ return []; } }
+function chatClear(){
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const c=$('#fCap'); if(c){ c.value=0; const v=$('#capVal'); if(v)v.textContent='any'; }
+  syncChips(); render();
+}
+function applyChat(raw){
+  const msg=$('#chatmsg'); const s=' '+String(raw||'').toLowerCase().replace(/\s+/g,' ').trim()+' ';
+  if(!s.trim())return;
+  if(/^\s*(clear|reset|all|everything|start over|show all)\s*$/.test(s)){ chatClear(); if(msg)msg.textContent='Cleared'; return; }
+  // fresh search — clear chat-controlled fields + their drawer inputs first
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const a={}; const got=[]; let m;
+  // warrantability / condo
+  if(/unwarrantable|non-?warrantable|not warrantable/.test(s)){ F.unwarrantable=true; got.push('unwarrantable condos'); }
+  if(/non-?qm\b|non qm/.test(s)){ F.nonqm=true; got.push('non-QM'); }
+  if(/\bcondos?\b/.test(s) && !F.unwarrantable){ F.types.add('Condo'); got.push('Condo'); }
+  // financing scenario (the seg now lives in the left drawer)
+  if(/\ball\s*cash\b/.test(s)){ a.scenario='cash'; got.push('all cash'); }
+  else if(/\b30\s*%?\s*down\b/.test(s)){ a.scenario='30'; got.push('30% down'); }
+  else if(/\b25\s*%?\s*down\b/.test(s)){ a.scenario='25'; got.push('25% down'); }
+  // units
+  const WORDU={duplex:2,triplex:3,fourplex:4,fiveplex:5};
+  for(const w in WORDU){ if(s.includes(' '+w)){ a.unitsMin=a.unitsMax=WORDU[w]; got.push(WORDU[w]+' units'); } }
+  if(m=s.match(/(\d+)\s*(?:-|to)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; a.unitsMax=+m[2]; got.push(m[1]+'–'+m[2]+' units'); }
+  else if(m=s.match(/(?:under|below|less than|up to|max(?:imum)?|<)\s*(\d+)\s*unit/)){ a.unitsMax=+m[1]; got.push('≤'+m[1]+' units'); }
+  else if(m=s.match(/(?:over|above|more than|at least|min(?:imum)?|\+|>)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; got.push(m[1]+'+ units'); }
+  else if(m=s.match(/(\d+)\s*unit/)){ a.unitsMin=a.unitsMax=+m[1]; got.push(m[1]+' units'); }
+  // type (set on F directly so Condo/multi don't get clobbered by applyAction's single-type set)
+  const types=[]; for(const [k,v] of CHAT_TYPES){ if(s.includes(' '+k) && !types.includes(v)) types.push(v); }
+  types.forEach(v=>F.types.add(v)); if(types.length) got.push(types.join('+'));
+  // city (known city names from the data; longest-first to avoid substring collisions)
+  for(const c of chatCities()){ const cl=c.toLowerCase(); if(s.includes(' '+cl+' ') || s.includes(' in '+cl)){ a.city=c; got.push(c); break; } }
+  // price
+  if((m=s.match(/\$?\s*([\d.,]+\s*[mk]?)\s*(?:-|to)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b/.test(m[0])){ a.minPrice=chatMoney(m[1]); a.maxPrice=chatMoney(m[2]); got.push('$'+m[1].trim()+'–$'+m[2].trim()); }
+  else if((m=s.match(/(?:under|below|less than|up to|<)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.maxPrice=chatMoney(m[1]); got.push('< $'+m[1].trim()); }
+  else if((m=s.match(/(?:over|above|more than|at least|>)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.minPrice=chatMoney(m[1]); got.push('> $'+m[1].trim()); }
+  // cap rate
+  if(m=s.match(/(\d+(?:\.\d+)?)\s*%?\s*cap/) || (m=s.match(/cap\s*(?:rate)?\s*(?:over|above|min|>|of)?\s*(\d+(?:\.\d+)?)/))){ a.capMin=+m[1]; got.push(m[1]+'%+ cap'); }
+  // year
+  if(m=s.match(/(?:built\s*after|newer than|after|since)\s*(\d{4})/)){ a.yearMin=+m[1]; got.push('after '+m[1]); }
+  if(m=s.match(/(?:built\s*before|older than|before)\s*(\d{4})/)){ a.yearMax=+m[1]; got.push('before '+m[1]); }
+  applyAction(a);   // maps a.* → F + drawer inputs, then syncChips()+render()
+  if(msg) msg.textContent = got.length ? ('Showing: '+got.join(' · ')) : ('Couldn’t parse — try “4 units in Los Angeles”');
+}
+(function(){ const q=$('#chatq'), go=$('#chatgo'), cl=$('#chatclear'), ex=$('#chatex');
+  if(q) q.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); applyChat(q.value); } });
+  if(go) go.addEventListener('click',()=>applyChat(q?q.value:''));
+  if(cl) cl.addEventListener('click',()=>{ if(q)q.value=''; chatClear(); const m=$('#chatmsg'); if(m)m.textContent='Cleared'; });
+  if(ex) ex.addEventListener('click',e=>{ const c=e.target.closest('.chatchip'); if(!c)return; if(q)q.value=c.dataset.q; applyChat(c.dataset.q); });
+})();
+
 // ---- persistence ----
 function saveState(){ try{ localStorage.setItem('cre_filters', JSON.stringify({
   types:[...F.types],firms:[...F.firms],cityQuery:F.cityQuery,priceMin:F.priceMin,priceMax:F.priceMax,

← e53f98e CRCP: move financing scenario control (25%/30%/All cash) fro  ·  back to Commercialrealestate  ·  CRCP root: center the top chat bar (constrained width, cente 884b8b7 →