676 lines
24 KiB
HTML
676 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>DOP — GBP Snapshot Intake v1.3</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1419;
|
||
--panel: #1a2332;
|
||
--border: #2d3a4f;
|
||
--text: #e7ecf3;
|
||
--muted: #8b9bb4;
|
||
--accent: #3b82f6;
|
||
--accent-hover: #2563eb;
|
||
--success: #22c55e;
|
||
--input-bg: #0d1219;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.45;
|
||
}
|
||
.wrap {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
padding: 24px 16px 48px;
|
||
}
|
||
h1 {
|
||
font-size: 1.35rem;
|
||
margin: 0 0 4px;
|
||
font-weight: 650;
|
||
}
|
||
.sub {
|
||
color: var(--muted);
|
||
font-size: 0.92rem;
|
||
margin-bottom: 20px;
|
||
}
|
||
.card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 16px 16px 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.card h2 {
|
||
font-size: 0.85rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--muted);
|
||
margin: 0 0 12px;
|
||
font-weight: 600;
|
||
}
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px 14px;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.grid { grid-template-columns: 1fr; }
|
||
}
|
||
.full { grid-column: 1 / -1; }
|
||
label {
|
||
display: block;
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
margin-bottom: 4px;
|
||
}
|
||
input[type="text"],
|
||
input[type="url"],
|
||
select,
|
||
textarea {
|
||
width: 100%;
|
||
background: var(--input-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
padding: 9px 11px;
|
||
font-size: 0.95rem;
|
||
outline: none;
|
||
}
|
||
input:focus, select:focus, textarea:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
textarea { min-height: 72px; resize: vertical; }
|
||
.actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin: 18px 0 10px;
|
||
}
|
||
button {
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 11px 16px;
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.btn-primary {
|
||
background: var(--accent);
|
||
color: white;
|
||
}
|
||
.btn-primary:hover { background: var(--accent-hover); }
|
||
.btn-secondary {
|
||
background: transparent;
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.btn-secondary:hover { border-color: var(--muted); }
|
||
.status {
|
||
font-size: 0.9rem;
|
||
color: var(--success);
|
||
min-height: 1.2em;
|
||
margin-bottom: 8px;
|
||
}
|
||
#output {
|
||
width: 100%;
|
||
min-height: 360px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
font-size: 0.82rem;
|
||
line-height: 1.4;
|
||
background: var(--input-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
color: var(--text);
|
||
padding: 12px;
|
||
white-space: pre;
|
||
}
|
||
.hint {
|
||
font-size: 0.82rem;
|
||
color: var(--muted);
|
||
margin-top: 8px;
|
||
}
|
||
.vocab-hint {
|
||
font-size: 0.78rem;
|
||
color: var(--muted);
|
||
font-style: italic;
|
||
margin-bottom: 6px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<h1>DOP — GBP Snapshot Intake v1.3</h1>
|
||
<p class="sub">Complete cold-audit form. <strong>NOT_PRESENT</strong> = looked, confirmed absent. <strong>NOT_OBSERVED</strong> = did not look / could not see. One primary category only. Paste output to Leonard — follows execution card v1.2 (R1–R6).</p>
|
||
|
||
<form id="gbpForm" autocomplete="off">
|
||
<!-- Snapshot Metadata -->
|
||
<div class="card">
|
||
<h2>Snapshot Metadata</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="client">Client name</label>
|
||
<input id="client" type="text" placeholder="Client name" />
|
||
</div>
|
||
<div>
|
||
<label for="snapshot_dt">Snapshot datetime + TZ (ISO 8601)</label>
|
||
<input id="snapshot_dt" type="text" placeholder="2026-08-04T14:30:00-07:00" />
|
||
</div>
|
||
<div>
|
||
<label for="source">Source</label>
|
||
<select id="source">
|
||
<option value="Human eye on Maps">Human eye on Maps</option>
|
||
<option value="Places API">Places API</option>
|
||
<option value="Owner dashboard">Owner dashboard</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="surface">Observation surface</label>
|
||
<select id="surface">
|
||
<option value="">— select —</option>
|
||
<option value="desktop_maps">desktop_maps</option>
|
||
<option value="mobile_maps">mobile_maps</option>
|
||
<option value="knowledge_panel">knowledge_panel</option>
|
||
<option value="other">other</option>
|
||
</select>
|
||
</div>
|
||
<div class="full">
|
||
<label for="maps_url">Maps or Place URL</label>
|
||
<input id="maps_url" type="url" placeholder="https://www.google.com/maps/place/..." />
|
||
</div>
|
||
<div>
|
||
<label for="place_id">Place ID</label>
|
||
<input id="place_id" type="text" placeholder="Leave blank — auto-computed from URL" />
|
||
</div>
|
||
<div>
|
||
<label for="computed_ids">Computed IDs (auto-filled)</label>
|
||
<input id="computed_ids" type="text" readonly placeholder="PLACE_ID · feature · CID" style="color:var(--muted)" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Core Fields -->
|
||
<div class="card">
|
||
<h2>Core Fields</h2>
|
||
<div class="vocab-hint">Copy exactly as shown. Use NOT_OBSERVED if you didn't see it.</div>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="business_name">Business name shown</label>
|
||
<input id="business_name" type="text" />
|
||
</div>
|
||
<div>
|
||
<label for="address">Address shown (exact string)</label>
|
||
<input id="address" type="text" />
|
||
</div>
|
||
<div>
|
||
<label for="address_canonical">Address canonical (optional normalize)</label>
|
||
<input id="address_canonical" type="text" placeholder="Suite not #, Drive not Dr" />
|
||
</div>
|
||
<div>
|
||
<label for="phone">Phone shown (full digits, no masking)</label>
|
||
<input id="phone" type="text" placeholder="+1 530-929-7120" />
|
||
</div>
|
||
<div>
|
||
<label for="website">Website shown</label>
|
||
<input id="website" type="text" />
|
||
</div>
|
||
<div>
|
||
<label for="primary_category">Primary category <strong>(ONE canonical value)</strong></label>
|
||
<input id="primary_category" type="text" placeholder="e.g. Gym · Physical fitness program · Personal trainer" />
|
||
</div>
|
||
<div class="full">
|
||
<label for="additional_categories">Additional categories (comma-separated)</label>
|
||
<input id="additional_categories" type="text" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reputation -->
|
||
<div class="card">
|
||
<h2>Reputation</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="star_rating">Star rating</label>
|
||
<input id="star_rating" type="text" placeholder="e.g. 4.8" />
|
||
</div>
|
||
<div>
|
||
<label for="review_count">Review count</label>
|
||
<input id="review_count" type="text" placeholder="e.g. 42" />
|
||
</div>
|
||
<div>
|
||
<label for="review_responses">Review responses observed?</label>
|
||
<select id="review_responses">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="review_responses_detail">If Yes: N of total with replies + most recent age</label>
|
||
<input id="review_responses_detail" type="text" placeholder="e.g. 3 of 5, most recent 12d ago" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ownership / Claim Signals -->
|
||
<div class="card">
|
||
<h2>Ownership / Claim Signals</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="claim_prompt">"Own this business?" prompt visible</label>
|
||
<select id="claim_prompt">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="No">No</option>
|
||
<option value="Yes">Yes</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="claimed_status">Claimed status</label>
|
||
<select id="claimed_status">
|
||
<option value="Unknown">Unknown</option>
|
||
<option value="Claimed">Claimed</option>
|
||
<option value="Unclaimed">Unclaimed</option>
|
||
<option value="Likely Claimed — inferred">Likely Claimed — inferred</option>
|
||
</select>
|
||
</div>
|
||
<div class="full">
|
||
<label for="claimed_signals">If inferred — signals used</label>
|
||
<input id="claimed_signals" type="text" placeholder="e.g. No claim prompt; review responses observed" />
|
||
</div>
|
||
<div>
|
||
<label for="verified_badge">Verified badge observed?</label>
|
||
<select id="verified_badge">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Operations -->
|
||
<div class="card">
|
||
<h2>Operations</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="hours_listed">Hours listed</label>
|
||
<select id="hours_listed">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="open_now">Open now indicator (valid only with datetime+TZ)</label>
|
||
<select id="open_now">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
<div class="full">
|
||
<label for="hours_text">Hours text</label>
|
||
<input id="hours_text" type="text" placeholder="e.g. Mon–Fri 5:30 AM–6:30 PM, Sat 8–10 AM, Sun Closed" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Conversion (Action Button Row) -->
|
||
<div class="card">
|
||
<h2>Conversion — Complete the whole row if any button observed</h2>
|
||
<div class="vocab-hint">Map the full action-button row on the GBP panel. If you see one, capture all.</div>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="booking_button">Booking button <strong>on the GBP</strong></label>
|
||
<select id="booking_button">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="booking_label">Booking button label</label>
|
||
<input id="booking_label" type="text" placeholder="e.g. Book appointment" />
|
||
</div>
|
||
<div>
|
||
<label for="messaging_button">Messaging button present</label>
|
||
<select id="messaging_button">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="call_button">Call button present</label>
|
||
<select id="call_button">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="directions_button">Directions button present</label>
|
||
<select id="directions_button">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
|
||
</select>
|
||
</div>
|
||
<div class="full">
|
||
<label for="other_action_buttons">Other action buttons observed</label>
|
||
<input id="other_action_buttons" type="text" placeholder="e.g. Order, Sign up, Get offer" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Content Surface -->
|
||
<div class="card">
|
||
<h2>Content Surface</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="photos_present">Photos present</label>
|
||
<select id="photos_present">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="photo_count">Approx photo count</label>
|
||
<input id="photo_count" type="text" placeholder="e.g. 6" />
|
||
</div>
|
||
<div class="full">
|
||
<label for="services_listed">Services listed (exact labels)</label>
|
||
<input id="services_listed" type="text" placeholder="e.g. strength training only" />
|
||
</div>
|
||
<div>
|
||
<label for="posts_section_present">Posts / Updates section present</label>
|
||
<select id="posts_section_present">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="post_count">Post count (0 if empty)</label>
|
||
<input id="post_count" type="text" placeholder="0" />
|
||
</div>
|
||
<div>
|
||
<label for="most_recent_post_date">Most recent post date (optional)</label>
|
||
<input id="most_recent_post_date" type="text" />
|
||
</div>
|
||
<div>
|
||
<label for="qa_status">Q&A status</label>
|
||
<select id="qa_status">
|
||
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
|
||
<option value="NOT_PRESENT">NOT_PRESENT</option>
|
||
<option value="Visible but empty">Visible but empty</option>
|
||
<option value="Answers present">Answers present</option>
|
||
</select>
|
||
</div>
|
||
<div class="full">
|
||
<label for="attributes">Attributes / highlights visible (optional)</label>
|
||
<input id="attributes" type="text" placeholder="e.g. Wheelchair accessible, Women-owned, LGBTQ+ friendly" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Related Listings -->
|
||
<div class="card">
|
||
<h2>Related Listings</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="second_listing">Second related listing nearby</label>
|
||
<select id="second_listing">
|
||
<option value="Unknown">Unknown</option>
|
||
<option value="Yes">Yes</option>
|
||
<option value="No">No</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label for="second_listing_names">Related listing name(s)</label>
|
||
<input id="second_listing_names" type="text" />
|
||
</div>
|
||
<div class="full">
|
||
<label for="second_listing_notes">Related listing notes (factual only)</label>
|
||
<textarea id="second_listing_notes" placeholder="What relationship? Different suite? Same building?"></textarea>
|
||
</div>
|
||
<div class="full">
|
||
<label for="raw_notes">Raw notes (optional, factual only)</label>
|
||
<textarea id="raw_notes" placeholder="Anything else worth capturing — no analysis, just observation"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="actions">
|
||
<button type="button" class="btn-primary" id="btnGenerate">Generate for Leonard</button>
|
||
<button type="button" class="btn-secondary" id="btnCopy">Copy</button>
|
||
<button type="button" class="btn-secondary" id="btnDownload">Download .txt</button>
|
||
<button type="button" class="btn-secondary" id="btnClear">Clear</button>
|
||
</div>
|
||
<div class="status" id="status"></div>
|
||
|
||
<label for="output">Leonard-ready output</label>
|
||
<textarea id="output" readonly placeholder="Click Generate for Leonard…"></textarea>
|
||
<p class="hint">Follows execution card v1.2 (R1–R6). PLACE_ID and IDs auto-computed from URL. Paste output to Leonard with the instruction to follow the execution card exactly.</p>
|
||
</div>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
function val(id) {
|
||
const el = $(id);
|
||
const v = (el.value || "").trim();
|
||
if (v === "") return "NOT_OBSERVED";
|
||
// Normalize "No NOT_PRESENT" values
|
||
if (v === "No NOT_PRESENT") return "NOT_PRESENT";
|
||
return v;
|
||
}
|
||
|
||
function nowISO() {
|
||
const d = new Date();
|
||
const tz = -d.getTimezoneOffset();
|
||
const sign = tz >= 0 ? "+" : "-";
|
||
const pad = (n) => String(Math.abs(n)).padStart(2, "0");
|
||
const hh = pad(Math.floor(Math.abs(tz) / 60));
|
||
const mm = pad(Math.abs(tz) % 60);
|
||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}T${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:00${sign}${hh}:${mm}`;
|
||
}
|
||
|
||
// Pre-fill datetime
|
||
$("snapshot_dt").value = nowISO();
|
||
|
||
// Auto-compute PLACE_ID from MAPS_URL on input
|
||
function computePlaceId() {
|
||
const url = $("maps_url").value.trim();
|
||
const placeIdField = $("place_id");
|
||
const computedField = $("computed_ids");
|
||
|
||
if (!url) {
|
||
placeIdField.placeholder = "Leave blank — auto-computed from URL";
|
||
computedField.value = "";
|
||
return;
|
||
}
|
||
|
||
const parts = [];
|
||
|
||
// Extract place ID (/g/...)
|
||
const placeMatch = url.match(/(?:place\/|\/)g\/[a-zA-Z0-9_\-]+/);
|
||
if (placeMatch) parts.push("PLACE_ID: " + placeMatch[0].replace(/(place\/|\/)/, ""));
|
||
|
||
// Extract feature IDs (0x...:0x...)
|
||
const featureMatch = url.match(/0x[a-fA-F0-9]+:0x[a-fA-F0-9]+/g);
|
||
if (featureMatch) {
|
||
parts.push("feature: " + featureMatch[0]);
|
||
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1]);
|
||
// Fill place_id field with the first pair
|
||
placeIdField.value = featureMatch[0];
|
||
}
|
||
|
||
// Extract CID
|
||
const cidMatch = url.match(/[?&]cid=(\d+)/) || url.match(/0x[a-fA-F0-9]+:0x([a-fA-F0-9]+)/);
|
||
if (cidMatch && cidMatch[1]) {
|
||
const cid = cidMatch[1].startsWith("0x") ? String(parseInt(cidMatch[1], 16)) : cidMatch[1];
|
||
parts.push("CID: " + cid);
|
||
}
|
||
|
||
computedField.value = parts.join(" · ") || "";
|
||
if (!placeIdField.value && parts.length > 0) placeIdField.value = featureMatch ? featureMatch[0] : "";
|
||
}
|
||
|
||
$("maps_url").addEventListener("input", computePlaceId);
|
||
|
||
function buildOutput() {
|
||
// Auto-compute PLACE_ID before generating
|
||
computePlaceId();
|
||
|
||
const instructions = `Follow docs/agents/leonard-gbp-execution-card.md exactly (v1.2).
|
||
Map intake into client Data Inventory GBP row.
|
||
Apply only threat trigger rules R1–R6.
|
||
Do not invent fields. Do not add solutions. Do not reason beyond the execution card.
|
||
Return: updated GBP section, any threat changes, Changes Made list (with object IDs), Rules Fired list (rule ID, version, inputs, values, output).`;
|
||
|
||
const metadata = [
|
||
`CLIENT: ${val("client")}`,
|
||
`SNAPSHOT_DATETIME: ${val("snapshot_dt")}`,
|
||
`SOURCE: ${val("source")}`,
|
||
`OBSERVATION_SURFACE: ${val("surface")}`,
|
||
`MAPS_URL: ${val("maps_url")}`,
|
||
`PLACE_ID: ${val("place_id")}`
|
||
].join("\n");
|
||
|
||
const core = [
|
||
`BUSINESS_NAME: ${val("business_name")}`,
|
||
`ADDRESS: ${val("address")}`,
|
||
`ADDRESS_CANONICAL: ${val("address_canonical")}`,
|
||
`PHONE: ${val("phone")}`,
|
||
`WEBSITE: ${val("website")}`,
|
||
`PRIMARY_CATEGORY: ${val("primary_category")}`,
|
||
`ADDITIONAL_CATEGORIES: ${val("additional_categories")}`
|
||
].join("\n");
|
||
|
||
const reputation = [
|
||
`STAR_RATING: ${val("star_rating")}`,
|
||
`REVIEW_COUNT: ${val("review_count")}`,
|
||
`REVIEW_RESPONSES_OBSERVED: ${val("review_responses")}`,
|
||
`REVIEW_RESPONSES_DETAIL: ${val("review_responses_detail")}`
|
||
].join("\n");
|
||
|
||
const claim = [
|
||
`CLAIM_PROMPT_VISIBLE: ${val("claim_prompt")}`,
|
||
`CLAIMED_STATUS: ${val("claimed_status")}`,
|
||
`CLAIMED_SIGNALS: ${val("claimed_signals")}`,
|
||
`VERIFIED_BADGE_VISIBLE: ${val("verified_badge")}`
|
||
].join("\n");
|
||
|
||
const operations = [
|
||
`HOURS_LISTED: ${val("hours_listed")}`,
|
||
`HOURS_TEXT: ${val("hours_text")}`,
|
||
`OPEN_NOW_OBSERVED: ${val("open_now")}`
|
||
].join("\n");
|
||
|
||
const conversion = [
|
||
`BOOKING_BUTTON_PRESENT_ON_GBP: ${val("booking_button")}`,
|
||
`BOOKING_BUTTON_LABEL: ${val("booking_label")}`,
|
||
`MESSAGING_BUTTON_PRESENT: ${val("messaging_button")}`,
|
||
`CALL_BUTTON_PRESENT: ${val("call_button")}`,
|
||
`DIRECTIONS_BUTTON_PRESENT: ${val("directions_button")}`,
|
||
`OTHER_ACTION_BUTTONS: ${val("other_action_buttons")}`
|
||
].join("\n");
|
||
|
||
const content = [
|
||
`PHOTOS_PRESENT: ${val("photos_present")}`,
|
||
`PHOTO_COUNT_APPROX: ${val("photo_count")}`,
|
||
`SERVICES_LISTED: ${val("services_listed")}`,
|
||
`POSTS_SECTION_PRESENT: ${val("posts_section_present")}`,
|
||
`POST_COUNT: ${val("post_count")}`,
|
||
`MOST_RECENT_POST_DATE: ${val("most_recent_post_date")}`,
|
||
`QA_STATUS: ${val("qa_status")}`,
|
||
`ATTRIBUTES_HIGHLIGHTS: ${val("attributes")}`
|
||
].join("\n");
|
||
|
||
const related = [
|
||
`SECOND_LISTING_NEARBY: ${val("second_listing")}`,
|
||
`SECOND_LISTING_NAMES: ${val("second_listing_names")}`,
|
||
`SECOND_LISTING_NOTES: ${val("second_listing_notes")}`,
|
||
`RAW_NOTES: ${val("raw_notes")}`
|
||
].join("\n");
|
||
|
||
return `${instructions}
|
||
|
||
--- GBP SNAPSHOT INTAKE v1.3 ---
|
||
${metadata}
|
||
|
||
${core}
|
||
|
||
${reputation}
|
||
|
||
${claim}
|
||
|
||
${operations}
|
||
|
||
${conversion}
|
||
|
||
${content}
|
||
|
||
${related}
|
||
`;
|
||
}
|
||
|
||
function setStatus(msg) {
|
||
$("status").textContent = msg;
|
||
if (msg) setTimeout(() => {
|
||
if ($("status").textContent === msg) $("status").textContent = "";
|
||
}, 3000);
|
||
}
|
||
|
||
$("btnGenerate").addEventListener("click", () => {
|
||
$("output").value = buildOutput();
|
||
setStatus("Generated.");
|
||
});
|
||
|
||
$("btnCopy").addEventListener("click", async () => {
|
||
const text = $("output").value || buildOutput();
|
||
$("output").value = text;
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
setStatus("Copied.");
|
||
} catch (e) {
|
||
$("output").focus();
|
||
$("output").select();
|
||
setStatus("Select-all ready — Ctrl/Cmd+C.");
|
||
}
|
||
});
|
||
|
||
$("btnDownload").addEventListener("click", () => {
|
||
const text = $("output").value || buildOutput();
|
||
$("output").value = text;
|
||
const client = ($("client").value || "client").trim().replace(/[^\w\-]+/g, "_") || "client";
|
||
const date = ($("snapshot_dt").value || "").split("T")[0] || "nodate";
|
||
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = `gbp-intake-${client}-${date}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(a.href);
|
||
setStatus("Downloaded.");
|
||
});
|
||
|
||
$("btnClear").addEventListener("click", () => {
|
||
$("gbpForm").reset();
|
||
$("snapshot_dt").value = nowISO();
|
||
$("computed_ids").value = "";
|
||
$("output").value = "";
|
||
setStatus("Cleared.");
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |