Benjamin Harris 2 mesi fa
parent
commit
0aac59d901
1 ha cambiato i file con 148 aggiunte e 22 eliminazioni
  1. 148 22
      public/index.php

+ 148 - 22
public/index.php

@@ -149,6 +149,13 @@
         <button class="qa-pill" data-qa="Create a setbacks table (front/side/rear) for the Village Zone with A vs P and sources."><i class="bi bi-table"></i> Setbacks table</button>
       </div>
 
+      <!-- Active property context indicator (shows when arriving from site-report.php) -->
+      <div id="heroCtxBar" style="display:none;margin-bottom:12px;padding:8px 14px;background:rgba(45,220,138,0.07);border:1px solid rgba(45,220,138,0.2);border-radius:8px;font-size:0.8rem;color:var(--text-secondary);align-items:center;gap:8px;flex-wrap:wrap;">
+        <i class="bi bi-geo-alt" style="color:var(--accent);flex-shrink:0;"></i>
+        <span id="heroCtxText"></span>
+        <a href="#" id="heroCtxClear" style="margin-left:auto;color:var(--text-muted);font-size:0.72rem;">clear</a>
+      </div>
+
       <!-- Search form -->
       <form id="heroAskForm" class="search-wrap">
         <i class="bi bi-search search-icon"></i>
@@ -157,6 +164,13 @@
                autocomplete="off" />
         <button class="search-btn" type="submit">Ask</button>
       </form>
+
+      <!-- Address detection hint -->
+      <div id="heroAddressBar" style="display:none;margin-top:8px;padding:6px 14px;background:rgba(45,220,138,0.06);border:1px solid rgba(45,220,138,0.15);border-radius:6px;font-size:0.78rem;color:var(--text-secondary);align-items:center;gap:8px;">
+        <i class="bi bi-geo-alt" style="color:var(--accent);flex-shrink:0;"></i>
+        Looks like an address — <a href="/site-report.php" style="color:var(--accent);">look up property data</a> first for zone &amp; overlay context.
+      </div>
+
       <p class="search-hint">
         Press ⏎ or ⌘/Ctrl+⏎ to search. Or
         <a href="#" id="openDemoLink">open the interactive demo</a>.
@@ -449,12 +463,21 @@
           <button class="sample-btn" type="button">Do overlays apply to 19 Main Street, Hobart (example)? Which clauses should be checked?</button>
         </div>
         <div style="margin-top:20px;">
+          <div style="margin-bottom:10px;">
+            <div class="sample-label" style="margin-bottom:5px;">Council (optional)</div>
+            <select id="demoCouncil" style="width:100%;background:var(--bg-card);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-family:inherit;font-size:0.82rem;padding:7px 10px;outline:none;">
+              <option value="">SPP only</option>
+            </select>
+          </div>
+          <div id="demoCtxBar" style="display:none;padding:8px 10px;background:rgba(45,220,138,0.07);border:1px solid rgba(45,220,138,0.2);border-radius:6px;font-size:0.75rem;color:var(--text-secondary);margin-bottom:10px;">
+            <i class="bi bi-geo-alt" style="color:var(--accent);margin-right:5px;"></i><span id="demoCtxText"></span>
+          </div>
           <div class="tps-toggle">
             <label class="toggle-track">
               <input type="checkbox" id="demoAllowTPS" checked>
               <span class="toggle-knob"></span>
             </label>
-            <span class="toggle-label">Allow Tasmanian Planning Scheme references</span>
+            <span class="toggle-label">Include State Planning Provisions (SPP)</span>
           </div>
           <button id="demoRunBtn" class="btn btn-primary" style="width:100%;justify-content:center;"><i class="bi bi-play"></i> Run demo</button>
           <p class="api-note">API: <code id="demoApiBase"></code></p>
@@ -524,25 +547,20 @@
             return h;
           }
          
-          // Unified fetch — tries multiple body shapes, returns parsed data
-          async function askAPI(question, allowTPS = true) {
-            const shapes = [
-              { query: question, allowTPS },
-              { question, allowTPS },
-              { q: question, allowTPS },
-              { prompt: question, allowTPS }
-            ];
-            let lastErr;
-            for (const body of shapes) {
-              try {
-                const res  = await fetch(API_BASE, { method: 'POST', headers: authHeaders(), body: JSON.stringify(body) });
-                const text = await res.text();
-                if (!res.ok) throw new Error(text || 'HTTP ' + res.status);
-                try { return JSON.parse(text); }
-                catch (_) { return { answer: text, sources: [] }; }
-              } catch (e) { lastErr = e; }
-            }
-            throw lastErr || new Error('Unknown error');
+          // Fetch from /ask with proper field names matching the backend
+          async function askAPI(question, scope = 'state_only', council = null) {
+            const body = { query: question, top_k: 8, scope };
+            if (council) body.council = council;
+            const res  = await fetch(API_BASE, {
+              method: 'POST',
+              headers: authHeaders(),
+              credentials: 'omit',
+              body: JSON.stringify(body)
+            });
+            const text = await res.text();
+            if (!res.ok) throw new Error(text || 'HTTP ' + res.status);
+            try { return JSON.parse(text); }
+            catch (_) { return { answer: text, sources: [] }; }
           }
          
           // Render source chips into a container element
@@ -578,14 +596,69 @@
             heroStatus.style.display = on ? 'flex' : 'none';
           }
          
+          // ── Property context (from site-report.php via localStorage) ────
+          let heroCtx = null;
+          const ADDRESS_RE = /\b\d+[a-z]?\s+[a-z][a-z\s'-]{2,35}\b(?:\s+(?:street|st|road|rd|avenue|ave|drive|dr|court|ct|place|pl|crescent|cr|lane|ln|way|close|cl|circuit|grove|gr|terrace|tce|boulevard|blvd|highway|hwy))?/i;
+
+          function loadHeroCtx() {
+            try {
+              const raw = localStorage.getItem('tpr_builder_ctx');
+              if (!raw) return;
+              const { ctx, written_at } = JSON.parse(raw);
+              if (!ctx || (Date.now() - written_at > 30 * 60 * 1000)) return;
+              heroCtx = ctx;
+              // Hero context bar
+              const bar  = $('heroCtxBar');
+              const text = $('heroCtxText');
+              if (bar && text) {
+                const zones = Array.isArray(ctx.planning_zones) ? ctx.planning_zones.join(', ') : (ctx.planning_zones || '');
+                text.textContent = [ctx.address, ctx.council ? `Council: ${ctx.council}` : '', zones ? `Zone: ${zones}` : ''].filter(Boolean).join(' · ');
+                bar.style.display = 'flex';
+              }
+              // Demo modal context bar
+              const demoBar  = $('demoCtxBar');
+              const demoText = $('demoCtxText');
+              if (demoBar && demoText) {
+                demoText.textContent = ctx.address || '';
+                demoBar.style.display = '';
+              }
+            } catch(e) {}
+          }
+
+          function clearHeroCtx() {
+            heroCtx = null;
+            localStorage.removeItem('tpr_builder_ctx');
+            const bar = $('heroCtxBar');
+            if (bar) bar.style.display = 'none';
+            const demoBar = $('demoCtxBar');
+            if (demoBar) demoBar.style.display = 'none';
+          }
+
+          function buildHeroQuery(question) {
+            if (!heroCtx) return { query: question, scope: 'state_only', council: null };
+            const zones = Array.isArray(heroCtx.planning_zones) ? heroCtx.planning_zones.join(', ') : (heroCtx.planning_zones || '');
+            const codes = Array.isArray(heroCtx.planning_codes) ? heroCtx.planning_codes.join(', ') : (heroCtx.planning_codes || '');
+            const parts = [
+              heroCtx.address  ? `Address: ${heroCtx.address}`   : '',
+              heroCtx.council  ? `Council: ${heroCtx.council}`   : '',
+              zones            ? `Zone(s): ${zones}`              : '',
+              codes            ? `Codes/overlays: ${codes}`       : '',
+              heroCtx.area_sqm ? `Site area: ${heroCtx.area_sqm} m²` : '',
+            ].filter(Boolean).join('; ');
+            const council = heroCtx.council || null;
+            const scope   = council ? 'state_plus_local' : 'state_only';
+            return { query: `[Site context — ${parts}]\n\n${question}`, scope, council };
+          }
+
           async function runHeroQuery(question) {
             if (!question) { heroInput.focus(); return; }
             showResults();
             setHeroLoading(true);
             answerEl.textContent = '';
             sourcesEl.innerHTML  = '';
+            const { query, scope, council } = buildHeroQuery(question);
             try {
-              const data    = await askAPI(question);
+              const data    = await askAPI(query, scope, council);
               answerEl.textContent = data.answer || data.text || 'No answer received.';
               const sources = Array.isArray(data.sources) ? data.sources : (Array.isArray(data.citations) ? data.citations : []);
               renderSources(sourcesEl, sources);
@@ -679,8 +752,23 @@
             demoResults.textContent = '';
             setDemoLoading(true);
             telemetry('search_performed', { query: question, source: 'demo' });
+            const demoCouncilEl = $('demoCouncil');
+            const council = (demoCouncilEl?.value || heroCtx?.council || '').trim() || null;
+            const allowTPS = !!allowEl?.checked;
+            const scope = allowTPS ? (council ? 'state_plus_local' : 'state_only') : (council ? 'local_only' : 'any');
+            // Build query with site context if available
+            let demoQuestion = question;
+            if (heroCtx) {
+              const zones = Array.isArray(heroCtx.planning_zones) ? heroCtx.planning_zones.join(', ') : (heroCtx.planning_zones || '');
+              const parts = [
+                heroCtx.address  ? `Address: ${heroCtx.address}`  : '',
+                heroCtx.council  ? `Council: ${heroCtx.council}`  : '',
+                zones            ? `Zone(s): ${zones}`             : '',
+              ].filter(Boolean).join('; ');
+              demoQuestion = `[Site context — ${parts}]\n\n${question}`;
+            }
             try {
-              const data    = await askAPI(question, !!allowEl?.checked);
+              const data    = await askAPI(demoQuestion, scope, council);
               const msg     = data.answer || data.output || data.markdown || data.result || data.text || '';
               const sources = Array.isArray(data.sources) ? data.sources : (Array.isArray(data.citations) ? data.citations : []);
               demoResults.textContent = msg || JSON.stringify(data);
@@ -770,6 +858,44 @@
           // ── Footer year ──────────────────────────────────────────────
           const yearEl = $('year');
           if (yearEl) yearEl.textContent = new Date().getFullYear();
+
+          // ── Load councils into demo dropdown ─────────────────────────
+          (async () => {
+            try {
+              const apiRoot = API_BASE.replace(/\/ask\/?$/, '');
+              const res     = await fetch(`${apiRoot}/councils`, { cache: 'no-store' });
+              const items   = await res.json();
+              const sel     = $('demoCouncil');
+              if (sel && Array.isArray(items)) {
+                items.forEach(c => {
+                  const opt = document.createElement('option');
+                  opt.value = c; opt.textContent = c;
+                  sel.appendChild(opt);
+                });
+              }
+              // Auto-select council from property context if present
+              if (heroCtx?.council && sel) {
+                const opt = [...sel.options].find(o => o.value.toLowerCase() === heroCtx.council.toLowerCase());
+                if (opt) opt.selected = true;
+              }
+            } catch(e) {}
+          })();
+
+          // ── Property context init ────────────────────────────────────
+          loadHeroCtx();
+          $('heroCtxClear')?.addEventListener('click', e => { e.preventDefault(); clearHeroCtx(); });
+
+          // ── Address detection on hero input ──────────────────────────
+          heroInput?.addEventListener('input', () => {
+            const val = heroInput.value || '';
+            const bar = $('heroAddressBar');
+            if (!bar) return;
+            if (!heroCtx && val.length > 8 && ADDRESS_RE.test(val)) {
+              bar.style.display = 'flex';
+            } else {
+              bar.style.display = 'none';
+            }
+          });
          
           // ── Telemetry ────────────────────────────────────────────────
           const sid = localStorage.getItem('tpr_sid') || (() => {