feat: GBP Snapshot Form v1.3 — NOT_PRESENT/NOT_OBSERVED vocabulary, observation surface, address_canonical, Likely Claimed — inferred, R1–R6, auto-computed PLACE_ID from URL, complete action-button row, Download .txt, auto-datetime+TZ
This commit is contained in:
+596
-176
@@ -3,170 +3,447 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>DOP — GBP Snapshot Intake v1.2</title>
|
||||
<title>DOP — GBP Snapshot Intake v1.3</title>
|
||||
<style>
|
||||
:root { --bg:#0f1419; --panel:#1a2332; --border:#2d3a4f; --text:#e7ecf3; --muted:#8b9bb4; --accent:#3b82f6; --input:#0d1219; --ok:#22c55e; }
|
||||
: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: system-ui, 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.3rem; margin:0 0 4px; }
|
||||
.sub { color:var(--muted); font-size:0.9rem; margin-bottom:18px; }
|
||||
.card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px 16px 8px; margin-bottom:14px; }
|
||||
.card h2 { font-size:0.8rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--muted); margin:0 0 12px; }
|
||||
.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, select, textarea { width:100%; background:var(--input); 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:64px; 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:#fff; }
|
||||
.btn-secondary { background:transparent; color:var(--text); border:1px solid var(--border); }
|
||||
.status { font-size:0.9rem; color:var(--ok); min-height:1.2rem; }
|
||||
#output { width:100%; min-height:280px; font-family:ui-monospace,monospace; font-size:0.82rem; background:var(--input); 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; }
|
||||
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.2</h1>
|
||||
<p class="sub">NOT_PRESENT = looked, confirmed absent · NOT_OBSERVED = did not look / could not see. One primary category only. Paste output to Leonard (execution card v1.2).</p>
|
||||
<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" /></div>
|
||||
<div><label for="snapshot_dt">Snapshot datetime + TZ (ISO)</label><input id="snapshot_dt" type="text" placeholder="2026-08-03T14:30:00-07:00" /></div>
|
||||
<div><label for="source">Source</label>
|
||||
<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>Human eye on Maps</option>
|
||||
<option>Places API</option>
|
||||
<option>Owner dashboard</option>
|
||||
<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>
|
||||
<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 (blank = agent computes from URL)</label><input id="place_id" type="text" /></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</h2>
|
||||
<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)</label><input id="address" type="text" /></div>
|
||||
<div><label for="address_canonical">Address canonical (optional)</label><input id="address_canonical" type="text" placeholder="Suite not #" /></div>
|
||||
<div><label for="phone">Phone (full digits, no mask)</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 (ONE only)</label><input id="primary_category" type="text" placeholder="e.g. Gym" /></div>
|
||||
<div class="full"><label for="additional_categories">Additional categories (list)</label><input id="additional_categories" type="text" /></div>
|
||||
<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" /></div>
|
||||
<div><label for="review_count">Review count</label><input id="review_count" type="text" /></div>
|
||||
<div><label for="review_responses">Review responses observed</label>
|
||||
<select id="review_responses"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
<div>
|
||||
<label for="star_rating">Star rating</label>
|
||||
<input id="star_rating" type="text" placeholder="e.g. 4.8" />
|
||||
</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" /></div>
|
||||
<div>
|
||||
<label for="review_count">Review count</label>
|
||||
<input id="review_count" type="text" placeholder="e.g. 42" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Ownership / Claim</h2>
|
||||
<div class="grid">
|
||||
<div><label for="claim_prompt">Own this business? prompt</label>
|
||||
<select id="claim_prompt"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
</div>
|
||||
<div><label for="claimed">Claimed status</label>
|
||||
<select id="claimed">
|
||||
<option>Unknown</option>
|
||||
<option>Claimed</option>
|
||||
<option>Unclaimed</option>
|
||||
<option>Likely Claimed — inferred</option>
|
||||
<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 class="full"><label for="claimed_signals">If inferred — signals used</label><input id="claimed_signals" type="text" /></div>
|
||||
<div><label for="verified">Verified badge</label>
|
||||
<select id="verified"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
<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>No</option><option>Yes</option></select>
|
||||
</div>
|
||||
<div><label for="open_now">Open now (requires datetime+TZ)</label>
|
||||
<select id="open_now"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
</div>
|
||||
<div class="full"><label for="hours_text">Hours text</label><input id="hours_text" type="text" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Conversion (complete whole row if any button observed)</h2>
|
||||
<div class="grid">
|
||||
<div><label for="booking_button">Booking button on GBP</label>
|
||||
<select id="booking_button"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
</div>
|
||||
<div><label for="booking_label">Booking label</label><input id="booking_label" type="text" /></div>
|
||||
<div><label for="messaging_button">Messaging button</label>
|
||||
<select id="messaging_button"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
|
||||
</div>
|
||||
<div class="full"><label for="other_buttons">Other action buttons</label><input id="other_buttons" type="text" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Content</h2>
|
||||
<div class="grid">
|
||||
<div><label for="photos_present">Photos present</label>
|
||||
<select id="photos_present"><option>No</option><option>Yes</option></select>
|
||||
</div>
|
||||
<div><label for="photo_count">Approx photo count</label><input id="photo_count" type="text" /></div>
|
||||
<div class="full"><label for="services">Services listed (exact)</label><input id="services" type="text" /></div>
|
||||
<div><label for="posts_section">Posts section present</label>
|
||||
<select id="posts_section"><option>NOT_OBSERVED</option><option>Yes</option><option>No</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="recent_post">Most recent post date</label><input id="recent_post" type="text" /></div>
|
||||
<div><label for="qa_status">Q&A status</label>
|
||||
<select id="qa_status">
|
||||
<option>NOT_OBSERVED</option>
|
||||
<option>NOT_PRESENT</option>
|
||||
<option>empty</option>
|
||||
<option>answers present</option>
|
||||
<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 class="full"><label for="attributes">Attributes / highlights</label><input id="attributes" type="text" /></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>Related listings</h2>
|
||||
<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="second_listing">Second related listing nearby</label>
|
||||
<select id="second_listing"><option>Unknown</option><option>Yes</option><option>No</option></select>
|
||||
<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><label for="second_names">Related listing name(s)</label><input id="second_names" type="text" /></div>
|
||||
<div class="full"><label for="second_notes">Related notes (factual)</label><textarea id="second_notes"></textarea></div>
|
||||
<div class="full"><label for="raw_notes">Raw notes</label><textarea id="raw_notes"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -174,83 +451,226 @@
|
||||
<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></textarea>
|
||||
<p class="hint">Follows execution card v1.2 (R1–R6). PLACE_ID computed from URL by agent if blank.</p>
|
||||
</div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
function val(id) {
|
||||
const v = ($(id).value || '').trim();
|
||||
return v === '' ? 'NOT_OBSERVED' : v;
|
||||
}
|
||||
function buildOutput() {
|
||||
return `Follow docs/agents/leonard-gbp-execution-card.md exactly (v1.2).
|
||||
<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).
|
||||
Return: updated GBP section, any threat changes, Changes Made list (with object IDs), Rules Fired list (rule ID, version, inputs, values, output).`;
|
||||
|
||||
--- GBP SNAPSHOT INTAKE ---
|
||||
CLIENT: ${val('client')}
|
||||
SNAPSHOT_DATETIME: ${val('snapshot_dt')}
|
||||
SOURCE: ${val('source')}
|
||||
OBSERVATION_SURFACE: ${val('surface')}
|
||||
MAPS_URL: ${val('maps_url')}
|
||||
PLACE_ID: ${($('place_id').value || '').trim() || 'COMPUTE_FROM_URL'}
|
||||
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");
|
||||
|
||||
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')}
|
||||
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");
|
||||
|
||||
STAR_RATING: ${val('star_rating')}
|
||||
REVIEW_COUNT: ${val('review_count')}
|
||||
REVIEW_RESPONSES_OBSERVED: ${val('review_responses')}
|
||||
REVIEW_RESPONSES_DETAIL: ${val('review_responses_detail')}
|
||||
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");
|
||||
|
||||
CLAIM_PROMPT_VISIBLE: ${val('claim_prompt')}
|
||||
CLAIMED_STATUS: ${val('claimed')}
|
||||
CLAIMED_SIGNALS: ${val('claimed_signals')}
|
||||
VERIFIED_BADGE_VISIBLE: ${val('verified')}
|
||||
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");
|
||||
|
||||
HOURS_LISTED: ${val('hours_listed')}
|
||||
HOURS_TEXT: ${val('hours_text')}
|
||||
OPEN_NOW_OBSERVED: ${val('open_now')}
|
||||
const operations = [
|
||||
`HOURS_LISTED: ${val("hours_listed")}`,
|
||||
`HOURS_TEXT: ${val("hours_text")}`,
|
||||
`OPEN_NOW_OBSERVED: ${val("open_now")}`
|
||||
].join("\n");
|
||||
|
||||
BOOKING_BUTTON_PRESENT_ON_GBP: ${val('booking_button')}
|
||||
BOOKING_BUTTON_LABEL: ${val('booking_label')}
|
||||
MESSAGING_BUTTON_PRESENT: ${val('messaging_button')}
|
||||
OTHER_ACTION_BUTTONS: ${val('other_buttons')}
|
||||
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");
|
||||
|
||||
PHOTOS_PRESENT: ${val('photos_present')}
|
||||
PHOTO_COUNT_APPROX: ${val('photo_count')}
|
||||
SERVICES_LISTED: ${val('services')}
|
||||
POSTS_SECTION_PRESENT: ${val('posts_section')}
|
||||
POST_COUNT: ${val('post_count')}
|
||||
MOST_RECENT_POST_DATE: ${val('recent_post')}
|
||||
QA_STATUS: ${val('qa_status')}
|
||||
ATTRIBUTES_HIGHLIGHTS: ${val('attributes')}
|
||||
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");
|
||||
|
||||
SECOND_LISTING_NEARBY: ${val('second_listing')}
|
||||
SECOND_LISTING_NAMES: ${val('second_names')}
|
||||
SECOND_LISTING_NOTES: ${val('second_notes')}
|
||||
RAW_NOTES: ${val('raw_notes')}
|
||||
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}
|
||||
`;
|
||||
}
|
||||
$('btnGenerate').onclick = () => { $('output').value = buildOutput(); $('status').textContent = 'Generated.'; };
|
||||
$('btnCopy').onclick = async () => {
|
||||
const t = $('output').value || buildOutput();
|
||||
$('output').value = t;
|
||||
try { await navigator.clipboard.writeText(t); $('status').textContent = 'Copied.'; } catch { $('status').textContent = 'Select-all + copy.'; }
|
||||
};
|
||||
$('btnClear').onclick = () => { $('gbpForm').reset(); $('output').value = ''; $('status').textContent = 'Cleared.'; };
|
||||
</script>
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user