|
|
@@ -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 & 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') || (() => {
|