Benjamin Harris преди 2 месеца
родител
ревизия
c686984079
променени са 2 файла, в които са добавени 163 реда и са изтрити 33 реда
  1. 131 8
      public/local_state-planning-scheme.php
  2. 32 25
      public/site-report.php

+ 131 - 8
public/local_state-planning-scheme.php

@@ -354,6 +354,30 @@
       font-size: 0.7rem; color: var(--text-muted); margin-top: 7px;
     }
 
+    /* Property context panel */
+    .prop-ctx-panel {
+      background: var(--accent-dim); border: 1px solid rgba(45,220,138,0.2);
+      border-radius: var(--radius-sm); padding: 10px 12px;
+      font-size: 0.78rem;
+    }
+    .prop-ctx-addr {
+      color: var(--text-primary); font-weight: 500; margin-bottom: 3px;
+      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+    }
+    .prop-ctx-meta { color: var(--text-muted); font-size: 0.72rem; line-height: 1.6; }
+    .prop-ctx-meta span { display: block; }
+
+    /* Address suggestion bar */
+    .address-suggest-bar {
+      background: rgba(45,220,138,0.06); border: 1px solid rgba(45,220,138,0.18);
+      border-radius: var(--radius-sm); padding: 6px 12px;
+      font-size: 0.75rem; color: var(--text-secondary);
+      margin-bottom: 8px; display: none; align-items: center;
+      gap: 8px; max-width: 800px;
+    }
+    .address-suggest-bar.show { display: flex; }
+    .address-suggest-bar a { color: var(--accent); text-decoration: underline; }
+
     /* Synonym suggestion */
     .synonym-bar {
       background: var(--accent-dim); border: 1px solid rgba(45,220,138,0.2);
@@ -501,6 +525,17 @@
       </label>
     </div>
 
+    <!-- Active property context (populated when arriving from site-report.php) -->
+    <div class="sidebar-section" id="propCtxSection" style="display:none;">
+      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
+        <span class="sidebar-label" style="margin:0;">Active property</span>
+        <button id="propCtxDismiss" class="btn btn-ghost" style="padding:2px 8px;font-size:0.68rem;">
+          <i class="bi bi-x"></i> Clear
+        </button>
+      </div>
+      <div class="prop-ctx-panel" id="propCtxPanel"></div>
+    </div>
+
     <!-- Quick asks -->
     <div class="sidebar-section">
       <span class="sidebar-label">Quick queries</span>
@@ -562,6 +597,10 @@
 
     <!-- Input -->
     <div class="input-bar">
+      <div class="address-suggest-bar" id="addressSuggestBar">
+        <i class="bi bi-geo-alt" style="color:var(--accent);flex-shrink:0;"></i>
+        <span id="addressSuggestText">Looks like an address — <a id="addressSuggestLink" href="/site-report.php">look up property data</a> for zone &amp; overlay context, then return here.</span>
+      </div>
       <div class="synonym-bar" id="synonymBar"></div>
       <div class="input-wrap">
         <textarea
@@ -632,6 +671,55 @@ const chatEmpty  = byId('chatEmpty');
 const questionEl = byId('question');
 const askBtn     = byId('askBtn');
 
+/* ── Property context (arrives via localStorage from site-report.php) ── */
+let propCtx = null;
+
+function loadPropCtx() {
+  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; // 30-min TTL
+    propCtx = ctx;
+    renderPropCtx();
+    // Auto-set council dropdown once options are loaded
+    if (ctx.council) {
+      const trySet = setInterval(() => {
+        const sel = byId('council');
+        const opt = sel && [...sel.options].find(o =>
+          o.value.toLowerCase() === ctx.council.toLowerCase()
+        );
+        if (opt) { opt.selected = true; clearInterval(trySet); }
+      }, 100);
+      setTimeout(() => clearInterval(trySet), 5000);
+    }
+  } catch(e) { console.warn('[propCtx] load failed', e); }
+}
+
+function renderPropCtx() {
+  const section = byId('propCtxSection');
+  const panel   = byId('propCtxPanel');
+  if (!section || !panel || !propCtx) return;
+  const zones = Array.isArray(propCtx.planning_zones) ? propCtx.planning_zones.join(', ') : (propCtx.planning_zones || '');
+  const codes = Array.isArray(propCtx.planning_codes) ? propCtx.planning_codes.join(', ') : (propCtx.planning_codes || '');
+  panel.innerHTML = `
+    <div class="prop-ctx-addr"><i class="bi bi-geo-alt" style="color:var(--accent);margin-right:4px;"></i>${esc(propCtx.address || '—')}</div>
+    <div class="prop-ctx-meta">
+      ${propCtx.council   ? `<span>Council: ${esc(propCtx.council)}</span>` : ''}
+      ${zones             ? `<span>Zone: ${esc(zones)}</span>` : ''}
+      ${codes             ? `<span>Codes: ${esc(codes)}</span>` : ''}
+      ${propCtx.area_sqm  ? `<span>Area: ${esc(String(propCtx.area_sqm))} m²</span>` : ''}
+    </div>`;
+  section.style.display = '';
+}
+
+function clearPropCtx() {
+  propCtx = null;
+  localStorage.removeItem('tpr_builder_ctx');
+  const section = byId('propCtxSection');
+  if (section) section.style.display = 'none';
+}
+
 /* ── Session ID for telemetry ────────────────────────────────────────── */
 const SID_KEY = 'tpr_sid';
 const sid = localStorage.getItem(SID_KEY) || (() => {
@@ -767,8 +855,23 @@ async function callExternalLLM(prompt, provider) {
 
 /* ── Ask ─────────────────────────────────────────────────────────────── */
 async function ask(queryOverride) {
-  const query   = (queryOverride || questionEl.value || '').trim();
-  if (!query || isAsking) return;
+  const rawQuery = (queryOverride || questionEl.value || '').trim();
+  if (!rawQuery || isAsking) return;
+
+  // Prepend site context if a property has been looked up
+  let query = rawQuery;
+  if (propCtx) {
+    const zones = Array.isArray(propCtx.planning_zones) ? propCtx.planning_zones.join(', ') : (propCtx.planning_zones || '');
+    const codes = Array.isArray(propCtx.planning_codes) ? propCtx.planning_codes.join(', ') : (propCtx.planning_codes || '');
+    const parts = [
+      propCtx.address       ? `Address: ${propCtx.address}`        : '',
+      propCtx.council       ? `Council: ${propCtx.council}`        : '',
+      zones                 ? `Zone(s): ${zones}`                  : '',
+      codes                 ? `Codes/overlays: ${codes}`           : '',
+      propCtx.area_sqm      ? `Site area: ${propCtx.area_sqm} m²`  : '',
+    ].filter(Boolean).join('; ');
+    query = `[Site context — ${parts}]\n\n${rawQuery}`;
+  }
 
   const council  = (byId('council')?.value || '').trim();
   const scope    = computeScope();
@@ -782,9 +885,9 @@ async function ask(queryOverride) {
   autoResize(questionEl);
   hideSynonymBar();
 
-  appendUserMsg(query);
+  appendUserMsg(rawQuery);
   const thinkEl = appendThinking();
-  sendEvent('search_performed', { query, scope, source: 'assistant', byok: useBYOK ? provider : null });
+  sendEvent('search_performed', { query: rawQuery, scope, source: 'assistant', byok: useBYOK ? provider : null });
 
   try {
     if (useBYOK) {
@@ -811,8 +914,8 @@ async function ask(queryOverride) {
         topk: lastSources.slice(0,10).map(s => ({ id:`${s.source_file}#p${s.page}`, score:s.score })),
       });
 
-      appendAssistantMsg(answer || 'No answer returned.', scope, lastSources, query, provider);
-      addToHistory(query);
+      appendAssistantMsg(answer || 'No answer returned.', scope, lastSources, rawQuery, provider);
+      addToHistory(rawQuery);
 
     } else {
       // ── Internal Ollama path (unchanged) ───────────────────────────
@@ -835,8 +938,8 @@ async function ask(queryOverride) {
         model: data.model || 'unknown', ok: true,
       });
 
-      appendAssistantMsg(data.answer || 'No answer returned.', scope, lastSources, query, 'internal');
-      addToHistory(query);
+      appendAssistantMsg(data.answer || 'No answer returned.', scope, lastSources, rawQuery, 'internal');
+      addToHistory(rawQuery);
     }
   } catch(e) {
     thinkEl.remove();
@@ -1050,6 +1153,7 @@ questionEl.addEventListener('keydown', e => {
 questionEl.addEventListener('input', () => {
   autoResize(questionEl);
   checkSynonyms(questionEl.value);
+  checkAddressInQuery(questionEl.value);
 });
 
 askBtn.addEventListener('click', () => ask());
@@ -1178,6 +1282,22 @@ function hideSynonymBar() {
   bar.innerHTML = '';
 }
 
+/* ── Address detection ───────────────────────────────────────────────── */
+// Matches patterns like "12 Smith Street" or "12a High Road Hobart"
+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 checkAddressInQuery(val) {
+  const bar = byId('addressSuggestBar');
+  if (!bar) return;
+  // Don't show if property context is already loaded
+  if (propCtx) { bar.classList.remove('show'); return; }
+  if (val.length > 8 && ADDRESS_RE.test(val)) {
+    bar.classList.add('show');
+  } else {
+    bar.classList.remove('show');
+  }
+}
+
 window.appendSynonym = function(term) {
   questionEl.value = (questionEl.value + ' ' + term).trim();
   questionEl.focus();
@@ -1228,7 +1348,10 @@ document.addEventListener('DOMContentLoaded', () => {
   loadCouncils();
   renderHistory();
   updateByokButton();
+  loadPropCtx();
   questionEl.focus();
+
+  byId('propCtxDismiss')?.addEventListener('click', clearPropCtx);
 });
 
 // Update button if user navigates back from settings with a new key

+ 32 - 25
public/site-report.php

@@ -544,6 +544,9 @@ $KEY_ENDPOINT    = './gmaps-key.php';
       <button id="pb-generate" class="btn btn-primary">
         <i class="bi bi-stars"></i> Generate AI report
       </button>
+      <button id="pb-ask-assistant" class="btn btn-outline">
+        <i class="bi bi-chat-dots"></i> Ask Assistant
+      </button>
       <button id="pb-send-builder" class="btn btn-outline">
         <i class="bi bi-layout-text-sidebar"></i> Open section builder
       </button>
@@ -1001,28 +1004,39 @@ $('md-copy')?.addEventListener('click', async () => {
   } catch { alert('Copy failed — select text and use Ctrl+C.'); }
 });
  
+/* ── Compose context (shared by section builder + assistant buttons) ── */
+function composeContext() {
+  const d = window._pbCurrentData || {};
+  return {
+    address: d.address || '', lat: d.lat || null, lng: d.lng || null,
+    pid: d.pid || null, title_id: d.title_id || null,
+    council: d.council || null, locality: d.locality || null,
+    planning_scheme: d.planning_scheme || null,
+    planning_zones: d.planning_zones || [],
+    planning_codes: d.planning_codes || [],
+    total_area: d.total_area || null, area_sqm: d.area_sqm || null,
+    area_ha: d.area_ha || null, tenure: d.tenure || null,
+    lpi: d.lpi || null, list_guid: d.list_guid || null,
+    map_png: d.map_png || null, proposal_summary: '', use_class: ''
+  };
+}
+
+/* ── Ask Assistant button ──────────────────────────────────────────── */
+$('pb-ask-assistant')?.addEventListener('click', () => {
+  if (!window._pbCurrentData) { showError('Look up a property first.'); return; }
+  const ctx = composeContext();
+  try {
+    localStorage.setItem('tpr_builder_ctx', JSON.stringify({ ctx, written_at: Date.now() }));
+  } catch(e) { console.warn('[SiteReport] localStorage write failed', e); }
+  window.location.href = '/local_state-planning-scheme.php';
+});
+
 /* ── Open section builder ──────────────────────────────────────────── */
 (function() {
   const btn = $('pb-send-builder');
   if (!btn) return;
   const bc = ('BroadcastChannel' in window) ? new BroadcastChannel('planning_ctx') : null;
- 
-  function composeContext() {
-    const d = window._pbCurrentData || {};
-    return {
-      address: d.address || '', lat: d.lat || null, lng: d.lng || null,
-      pid: d.pid || null, title_id: d.title_id || null,
-      council: d.council || null, locality: d.locality || null,
-      planning_scheme: d.planning_scheme || null,
-      planning_zones: d.planning_zones || [],
-      planning_codes: d.planning_codes || [],
-      total_area: d.total_area || null, area_sqm: d.area_sqm || null,
-      area_ha: d.area_ha || null, tenure: d.tenure || null,
-      lpi: d.lpi || null, list_guid: d.list_guid || null,
-      map_png: d.map_png || null, proposal_summary: '', use_class: ''
-    };
-  }
- 
+
   btn.addEventListener('click', () => {
     if (!window._pbCurrentData) { showError('Look up a property first.'); return; }
     const ctx = composeContext();
@@ -1031,20 +1045,13 @@ $('md-copy')?.addEventListener('click', async () => {
     // section-builder.php reads it immediately on load — no timing issues,
     // no message passing, no retry loops needed.
     try {
-      // localStorage is shared across tabs on the same origin.
-      // sessionStorage is tab-isolated — new tabs get an empty store.
       const payload = { ctx, written_at: Date.now() };
       localStorage.setItem('tpr_builder_ctx', JSON.stringify(payload));
-      console.log('[SiteReport] wrote to localStorage:', JSON.stringify(ctx).slice(0, 100));
     } catch(e) {
       console.warn('[SiteReport] localStorage write failed', e);
     }
 
-    
-    // TEMPORARY DIAGNOSTICS
-    console.log('[SiteReport] wrote to sessionStorage:', sessionStorage.getItem('tpr_builder_ctx')?.slice(0, 100));
- 
-    // Open the builder — it will find the context in sessionStorage
+    // Open the builder — it will find the context in localStorage
     const w = window.open('section-builder.php', '_blank', 'noopener');
  
     // Also send via postMessage as a fallback for same-tab / embedded use