Instrument webhooks, fix compose layout, and ship social connect.
Add Grafana-friendly webhook request logging, Quill overflow fix, New Contact flow, in-place postcard campaign compose, and functional social account connect + composer.
This commit is contained in:
@@ -6,28 +6,67 @@
|
||||
<div class="panel-h"><h2>Add account</h2></div>
|
||||
<div class="panel-b">
|
||||
<div class="connect-grid">
|
||||
<button type="button" class="connect-card" disabled>
|
||||
<a class="connect-card{% if connect_platform == 'facebook' %} is-active{% endif %}"
|
||||
href="{% url 'social:account_list' %}?connect=facebook">
|
||||
<span class="platform-icon meta">f</span>
|
||||
<div>
|
||||
<strong>Facebook Page</strong>
|
||||
<p>Connect a Page you manage. OAuth wiring comes next.</p>
|
||||
<p>Connect a Page you manage with a Page access token.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" class="connect-card" disabled>
|
||||
</a>
|
||||
<a class="connect-card{% if connect_platform == 'instagram' %} is-active{% endif %}"
|
||||
href="{% url 'social:account_list' %}?connect=instagram">
|
||||
<span class="platform-icon ig">IG</span>
|
||||
<div>
|
||||
<strong>Instagram Business</strong>
|
||||
<p>Requires a Facebook Page linked to an IG business account.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button type="button" class="connect-card" disabled>
|
||||
</a>
|
||||
<a class="connect-card{% if connect_platform == 'linkedin' %} is-active{% endif %}"
|
||||
href="{% url 'social:account_list' %}?connect=linkedin">
|
||||
<span class="platform-icon li">in</span>
|
||||
<div>
|
||||
<strong>LinkedIn</strong>
|
||||
<p>Personal or organization page. Tokens expire — re-auth when prompted.</p>
|
||||
<p>Personal or organization page. Tokens expire — reconnect when prompted.</p>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if connect_meta %}
|
||||
<div class="connect-form-panel" id="connect-form">
|
||||
<h3 style="margin:0 0 8px;font-size:16px">{{ connect_meta.title }}</h3>
|
||||
<ol class="connect-steps">
|
||||
{% for step in connect_meta.steps %}
|
||||
<li>{{ step }}</li>
|
||||
{% endfor %}
|
||||
{% if connect_meta.docs_url %}
|
||||
<li>Reference: <a href="{{ connect_meta.docs_url }}" target="_blank" rel="noopener">platform docs</a></li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
<form method="post" class="form-grid">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="connect">
|
||||
<input type="hidden" name="platform" value="{{ connect_platform }}">
|
||||
{% for name, label, placeholder in connect_meta.fields %}
|
||||
<div class="field">
|
||||
<label for="id_{{ name }}">{{ label }}</label>
|
||||
{% if name == 'access_token' %}
|
||||
<textarea id="id_{{ name }}" name="{{ name }}" required
|
||||
style="min-height:88px" placeholder="{{ placeholder }}"></textarea>
|
||||
{% else %}
|
||||
<input id="id_{{ name }}" name="{{ name }}"
|
||||
{% if name != 'page_id' and name != 'label' %}required{% endif %}
|
||||
placeholder="{{ placeholder }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" type="submit">Save account</button>
|
||||
<a class="btn btn-ghost" href="{% url 'social:account_list' %}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,10 +101,29 @@
|
||||
<span class="badge badge-failed">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="muted">Connect / disconnect in functionality pass</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<a class="btn btn-ghost btn-sm" href="{% url 'social:account_list' %}?connect={{ account.platform }}">Update tokens</a>
|
||||
{% if account.is_active %}
|
||||
<form method="post" style="display:inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="disconnect">
|
||||
<input type="hidden" name="account_id" value="{{ account.pk }}">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">Disconnect</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" style="display:inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="reactivate">
|
||||
<input type="hidden" name="account_id" value="{{ account.pk }}">
|
||||
<button class="btn btn-ghost btn-sm" type="submit">Mark active</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="empty-state">No accounts connected yet.</td></tr>
|
||||
<tr><td colspan="4" class="empty-state">No accounts connected yet — pick a platform above.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -84,11 +142,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Health checks</h2></div>
|
||||
<div class="panel-h"><h2>Next</h2></div>
|
||||
<div class="panel-b">
|
||||
<p class="muted">Token test buttons wire up with the connectors next.</p>
|
||||
<a class="btn btn-ghost btn-sm" href="{% url 'social:composer' %}">Open composer →</a>
|
||||
<a class="btn btn-primary btn-sm" href="{% url 'social:composer' %}">Open composer →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{% if connect_platform %}
|
||||
<script>
|
||||
document.getElementById('connect-form')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,285 +1,132 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Compose · Social{% endblock %}
|
||||
{% block topbar_title %}Compose & preview{% endblock %}
|
||||
{% block portal_content %}
|
||||
<div class="auth-alert" id="li-warn">
|
||||
<strong>LinkedIn needs re-auth.</strong> Token expired —
|
||||
<a href="{% url 'social:account_list' %}">Re-authorize in Accounts</a> before scheduling to LinkedIn.
|
||||
{% if not accounts %}
|
||||
<div class="auth-alert">
|
||||
<strong>No social accounts connected.</strong>
|
||||
<a href="{% url 'social:account_list' %}">Connect Facebook, Instagram, or LinkedIn</a> first.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="designer-layout with-ai">
|
||||
<div class="designer-controls">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Compose</h2><a class="btn btn-sm btn-ghost" href="{% url 'social:account_list' %}">Manage accounts</a></div>
|
||||
<div class="panel-b form-grid">
|
||||
<div class="field"><label>Caption</label>
|
||||
<textarea id="soc-caption" style="min-height:130px" oninput="syncSocial()">Open house this Saturday 11–1 at 412 Willow Lane. Quiet street, updated kitchen, walkable to Oakridge. DM or text me for details.
|
||||
|
||||
#JustListed #OpenHouse</textarea>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h">
|
||||
<h2>Compose</h2>
|
||||
<a class="btn btn-sm btn-ghost" href="{% url 'social:account_list' %}">Manage accounts</a>
|
||||
</div>
|
||||
<div class="panel-b">
|
||||
{% if error %}
|
||||
<ul class="portal-flash" style="margin:0 0 12px"><li class="error">{{ error }}</li></ul>
|
||||
{% endif %}
|
||||
<form method="post" class="form-grid" id="social-compose">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="id_body">Caption</label>
|
||||
<textarea id="id_body" name="body" style="min-height:140px"
|
||||
placeholder="Open house this Saturday…"
|
||||
oninput="syncPreview()">{{ form.body }}</textarea>
|
||||
<div class="hint"><span id="char-count">0</span> characters · IG soft limit ~2,200</div>
|
||||
</div>
|
||||
<div class="field"><label>Media</label>
|
||||
<select id="soc-media" onchange="syncSocial()">
|
||||
<option value="{% static "images/grid-layout-1-370x256.jpg" %}">Willow Lane exterior</option>
|
||||
<option value="{% static "images/grid-layout-2-370x256.jpg" %}">Kitchen</option>
|
||||
<option value="{% static "images/grid-layout-3-370x256.jpg" %}">Living room</option>
|
||||
<option value="">Text only (FB / LI)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Platforms</label>
|
||||
<div class="platform-toggles">
|
||||
<label class="toggle-pill"><input type="checkbox" id="plat-fb" checked onchange="syncSocial()"> Facebook</label>
|
||||
<label class="toggle-pill"><input type="checkbox" id="plat-ig" checked onchange="syncSocial()"> Instagram</label>
|
||||
<label class="toggle-pill warn"><input type="checkbox" id="plat-li" onchange="syncSocial()"> LinkedIn <span class="muted">(re-auth)</span></label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Accounts</label>
|
||||
{% for account in accounts %}
|
||||
<label class="check-row">
|
||||
<input type="checkbox" name="account_ids" value="{{ account.pk }}"
|
||||
{% if account.pk|stringformat:"s" in form.account_ids %}checked{% endif %}>
|
||||
{{ account.get_platform_display }} · {{ account.label }}
|
||||
</label>
|
||||
{% empty %}
|
||||
<p class="muted">No active accounts.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="form-grid cols-2">
|
||||
<div class="field"><label>Publish</label>
|
||||
<select id="soc-when-mode"><option>Schedule</option><option>Publish now</option></select>
|
||||
<div class="field">
|
||||
<label for="id_publish_mode">Publish</label>
|
||||
<select id="id_publish_mode" name="publish_mode">
|
||||
<option value="schedule"{% if form.publish_mode == "schedule" %} selected{% endif %}>Schedule</option>
|
||||
<option value="now"{% if form.publish_mode == "now" %} selected{% endif %}>Publish now</option>
|
||||
<option value="draft"{% if form.publish_mode == "draft" %} selected{% endif %}>Save draft only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>When</label><input type="datetime-local" value="2026-07-16T09:00"></div>
|
||||
</div>
|
||||
<div class="field"><label>Preview as</label>
|
||||
<div class="channel-tabs" style="margin:0">
|
||||
<a href="#" class="active" id="prev-fb" onclick="setPreview('fb');return false">Facebook</a>
|
||||
<a href="#" id="prev-ig" onclick="setPreview('ig');return false">Instagram</a>
|
||||
<a href="#" id="prev-li" onclick="setPreview('li');return false">LinkedIn</a>
|
||||
<div class="field">
|
||||
<label for="id_scheduled_for">When</label>
|
||||
<input id="id_scheduled_for" name="scheduled_for" type="datetime-local" step="60"
|
||||
value="{{ form.scheduled_for }}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" onclick="alert('Mockup: SocialPost + SocialPostTargets enqueued')">Schedule to selected platforms</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" type="submit" name="action" value="publish">Save / publish</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-assist-col">
|
||||
<div class="ai-chat">
|
||||
<div class="ai-chat-h">
|
||||
<h2>AI post assistant</h2>
|
||||
<span class="ai-pill">Beta mockup</span>
|
||||
</div>
|
||||
<div class="ai-chat-messages" id="ai-messages">
|
||||
<div class="ai-msg bot">
|
||||
<div class="who">Assistant</div>
|
||||
<div class="bubble">
|
||||
I can draft or refine captions for Facebook, Instagram, and LinkedIn.
|
||||
Tell me the listing, tone (warm / bold / luxury), and any must-include details —
|
||||
or tap a quick prompt below.
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>AI assist</h2></div>
|
||||
<div class="panel-b form-grid">
|
||||
<form method="post" class="form-grid">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="generate">
|
||||
<input type="hidden" name="publish_mode" value="{{ form.publish_mode }}">
|
||||
<input type="hidden" name="scheduled_for" value="{{ form.scheduled_for }}">
|
||||
{% for id in form.account_ids %}
|
||||
<input type="hidden" name="account_ids" value="{{ id }}">
|
||||
{% endfor %}
|
||||
<div class="field">
|
||||
<label for="id_prompt">Prompt</label>
|
||||
<textarea id="id_prompt" name="prompt" style="min-height:88px"
|
||||
placeholder="Warm open-house post for 412 Willow Lane…">{{ form.prompt }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-chat-prompts">
|
||||
<button type="button" class="chip" onclick="sendPrompt('Write an open-house post for 412 Willow Lane')">Open house draft</button>
|
||||
<button type="button" class="chip" onclick="sendPrompt('Make my caption shorter for Instagram')">Shorter for IG</button>
|
||||
<button type="button" class="chip" onclick="sendPrompt('Rewrite in a warmer, neighborly tone')">Warmer tone</button>
|
||||
<button type="button" class="chip" onclick="sendPrompt('Add 4 relevant hashtags')">Add hashtags</button>
|
||||
<button type="button" class="chip" onclick="sendPrompt('Make a LinkedIn-friendly professional version')">LinkedIn version</button>
|
||||
</div>
|
||||
<div class="ai-chat-input">
|
||||
<textarea id="ai-input" placeholder="Ask for a draft, rewrite, or hashtags…" rows="2"
|
||||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat();}"></textarea>
|
||||
<button class="btn btn-primary" type="button" onclick="sendChat()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="designer-preview-col">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Live post preview</h2><span class="muted" style="font-size:12px" id="prev-label">Facebook</span></div>
|
||||
<div class="panel-b" style="background:#e8edf3;display:flex;justify-content:center;padding:24px">
|
||||
<div class="social-phone" id="preview-fb">
|
||||
<div class="soc-header">
|
||||
<div class="soc-avatar">MR</div>
|
||||
<div>
|
||||
<div class="soc-name">Monica Dhillon · MKDRealtor.com</div>
|
||||
<div class="soc-meta">Sponsored · Just now · 🌐</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="soc-caption" id="fb-caption"></div>
|
||||
<div class="soc-image" id="fb-image"></div>
|
||||
<div class="soc-actions">Like · Comment · Share</div>
|
||||
</div>
|
||||
<div class="social-phone ig" id="preview-ig" style="display:none">
|
||||
<div class="soc-header">
|
||||
<div class="soc-avatar round">MR</div>
|
||||
<div>
|
||||
<div class="soc-name">monicadhillonhomes</div>
|
||||
<div class="soc-meta">Metro Area</div>
|
||||
</div>
|
||||
<div class="soc-more">•••</div>
|
||||
</div>
|
||||
<div class="soc-image square" id="ig-image"></div>
|
||||
<div class="soc-actions ig-act">♥ 💬 ✉</div>
|
||||
<div class="soc-caption"><strong>monicadhillonhomes</strong> <span id="ig-caption"></span></div>
|
||||
</div>
|
||||
<div class="social-phone li" id="preview-li" style="display:none">
|
||||
<div class="soc-header">
|
||||
<div class="soc-avatar sq">MR</div>
|
||||
<div>
|
||||
<div class="soc-name">Monica Dhillon</div>
|
||||
<div class="soc-meta">Realtor · 1st · Just now</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="soc-caption" id="li-caption"></div>
|
||||
<div class="soc-image" id="li-image"></div>
|
||||
<div class="soc-actions">Like · Comment · Repost · Send</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Scheduled</h2></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Post</th><th>Platforms</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Open house Willow Lane</td><td>FB · IG</td><td><span class="badge badge-scheduled">Scheduled</span></td></tr>
|
||||
<tr><td>Just sold — Birch Ave</td><td>FB · IG · LI</td><td><span class="badge badge-delivered">Published</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button class="btn btn-ghost" type="submit">Generate draft (Ollama)</button>
|
||||
</form>
|
||||
<p class="hint-block">Generated text fills the caption field. Review before publishing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="margin-top:20px">
|
||||
<div class="panel-h"><h2>Save draft (server)</h2></div>
|
||||
<div class="panel-b form-grid">
|
||||
{% if error %}<ul class="portal-flash"><li class="error">{{ error }}</li></ul>{% endif %}
|
||||
{% if saved %}<ul class="portal-flash"><li>Saved draft <a href="{% url 'social:post_detail' saved.pk %}">{{ saved.pk }}</a></li></ul>{% endif %}
|
||||
<form method="post" class="form-grid">
|
||||
{% csrf_token %}
|
||||
<div class="field"><label>AI prompt (optional)</label><textarea name="prompt">{{ prompt }}</textarea></div>
|
||||
<div class="field"><label>Platform hint</label>
|
||||
<select name="platform">
|
||||
<option value="">Any</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="linkedin">LinkedIn</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Body</label><textarea name="body" style="min-height:100px">{{ draft }}</textarea></div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-ghost" type="submit" name="action" value="generate">Generate draft</button>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="save">Save draft</button>
|
||||
<a class="btn btn-ghost" href="{% url 'social:post_list' %}">Scheduled / drafts</a>
|
||||
</div>
|
||||
</form>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Preview</h2></div>
|
||||
<div class="panel-b">
|
||||
<div class="preview-pane">
|
||||
<div class="muted" id="preview-empty">Preview updates as you type.</div>
|
||||
<div id="preview-content" hidden style="white-space:pre-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="font-size:13px;margin-top:12px">
|
||||
<a href="{% url 'social:post_list' %}">View recent posts</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
|
||||
let previewPlat = 'fb';
|
||||
let lastDraft = '';
|
||||
|
||||
function setPreview(p) {
|
||||
previewPlat = p;
|
||||
['fb','ig','li'].forEach(x => {
|
||||
document.getElementById('preview-' + x).style.display = x === p ? 'block' : 'none';
|
||||
document.getElementById('prev-' + x).classList.toggle('active', x === p);
|
||||
});
|
||||
document.getElementById('prev-label').textContent = ({fb:'Facebook',ig:'Instagram',li:'LinkedIn'})[p];
|
||||
}
|
||||
|
||||
function syncSocial() {
|
||||
const cap = document.getElementById('soc-caption').value;
|
||||
const media = document.getElementById('soc-media').value;
|
||||
document.getElementById('char-count').textContent = cap.length;
|
||||
document.getElementById('fb-caption').textContent = cap;
|
||||
document.getElementById('ig-caption').textContent = cap;
|
||||
document.getElementById('li-caption').textContent = cap;
|
||||
['fb','ig','li'].forEach(x => {
|
||||
const el = document.getElementById(x + '-image');
|
||||
if (!media) { el.style.display = 'none'; }
|
||||
else { el.style.display = 'block'; el.style.backgroundImage = 'url(' + media + ')'; }
|
||||
});
|
||||
document.getElementById('li-warn').style.display = document.getElementById('plat-li').checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
function appendMsg(role, html) {
|
||||
const root = document.getElementById('ai-messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ai-msg ' + role;
|
||||
div.innerHTML =
|
||||
'<div class="who">' + (role === 'user' ? 'You' : 'Assistant') + '</div>' +
|
||||
'<div class="bubble">' + html + '</div>';
|
||||
root.appendChild(div);
|
||||
root.scrollTop = root.scrollHeight;
|
||||
}
|
||||
|
||||
function draftActions(draft) {
|
||||
lastDraft = draft;
|
||||
const id = 'draft-' + Date.now();
|
||||
return (
|
||||
'<div class="ai-draft" id="' + id + '">' + escapeHtml(draft) + '</div>' +
|
||||
'<div class="ai-msg-actions">' +
|
||||
'<button type="button" class="btn btn-sm btn-primary" onclick="insertDraft(false)">Replace caption</button>' +
|
||||
'<button type="button" class="btn btn-sm btn-ghost" onclick="insertDraft(true)">Append</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function insertDraft(append) {
|
||||
const ta = document.getElementById('soc-caption');
|
||||
if (append && ta.value.trim()) ta.value = ta.value.replace(/\s*$/, '') + '\n\n' + lastDraft;
|
||||
else ta.value = lastDraft;
|
||||
syncSocial();
|
||||
}
|
||||
|
||||
function inventReply(prompt, current) {
|
||||
const p = prompt.toLowerCase();
|
||||
if (p.includes('shorter') || p.includes('instagram')) {
|
||||
return 'Open house Sat 11–1 · 412 Willow Lane\nUpdated kitchen, quiet street, walk to Oakridge.\nDM for details 🏡\n\n#JustListed #OpenHouse #WillowLane';
|
||||
(function () {
|
||||
function syncPreview() {
|
||||
var body = (document.getElementById('id_body') || {}).value || '';
|
||||
var count = document.getElementById('char-count');
|
||||
if (count) count.textContent = String(body.length);
|
||||
var empty = document.getElementById('preview-empty');
|
||||
var content = document.getElementById('preview-content');
|
||||
if (!empty || !content) return;
|
||||
if (!body) {
|
||||
empty.hidden = false;
|
||||
content.hidden = true;
|
||||
return;
|
||||
}
|
||||
empty.hidden = true;
|
||||
content.hidden = false;
|
||||
content.textContent = body;
|
||||
}
|
||||
if (p.includes('warm') || p.includes('neighbor')) {
|
||||
return 'I\'d love to show you around this Saturday.\n\n412 Willow Lane is open 11–1 — quiet street, bright kitchen, and an easy walk to Oakridge schools. Come say hi, no pressure.\n\nText me anytime if you want a private look first.';
|
||||
var mode = document.getElementById('id_publish_mode');
|
||||
var when = document.getElementById('id_scheduled_for');
|
||||
function syncWhen() {
|
||||
if (!mode || !when) return;
|
||||
when.disabled = mode.value !== 'schedule';
|
||||
}
|
||||
if (p.includes('hashtag')) {
|
||||
const base = current.trim() || 'Just listed — 412 Willow Lane. Open house Saturday 11–1.';
|
||||
return base.replace(/\s*#\S+/g, '').trim() + '\n\n#JustListed #OpenHouse #RealEstate #HomeTour';
|
||||
}
|
||||
if (p.includes('linkedin') || p.includes('professional')) {
|
||||
return 'Hosting an open house this Saturday, 11–1, at 412 Willow Lane.\n\nUpdated kitchen, walkable to Oakridge schools, asking $449,000. Happy to arrange a private showing for clients who can\'t make the window.\n\n#RealEstate #OpenHouse';
|
||||
}
|
||||
if (p.includes('open house') || p.includes('willow') || p.includes('draft')) {
|
||||
return 'Open house this Saturday 11–1 at 412 Willow Lane.\n\nQuiet street · updated kitchen · walkable to Oakridge.\nDM or text me for details — hope to see you there!\n\n#JustListed #OpenHouse';
|
||||
}
|
||||
if (p.includes('rewrite') || p.includes('improve') || current.trim()) {
|
||||
return 'Just listed: 412 Willow Lane.\n\nOpen Saturday 11–1. Updated kitchen, quiet street, walkable to Oakridge schools. Asking $449,000.\n\nMessage me for a private showing.\n\n#JustListed #OpenHouse';
|
||||
}
|
||||
return 'Tell me the address, price band, and vibe (warm / bold / luxury) and I\'ll draft a caption you can drop straight into the composer.';
|
||||
}
|
||||
|
||||
function respond(prompt) {
|
||||
const current = document.getElementById('soc-caption').value;
|
||||
const draft = inventReply(prompt, current);
|
||||
const needsDraft = !/tell me the address/i.test(draft);
|
||||
const body = needsDraft
|
||||
? 'Here\'s a draft you can use:' + draftActions(draft)
|
||||
: escapeHtml(draft);
|
||||
appendMsg('bot', body);
|
||||
}
|
||||
|
||||
function sendPrompt(text) {
|
||||
document.getElementById('ai-input').value = text;
|
||||
sendChat();
|
||||
}
|
||||
|
||||
function sendChat() {
|
||||
const input = document.getElementById('ai-input');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
appendMsg('user', escapeHtml(text));
|
||||
input.value = '';
|
||||
setTimeout(() => respond(text), 350);
|
||||
}
|
||||
|
||||
syncSocial();
|
||||
|
||||
if (mode) mode.addEventListener('change', syncWhen);
|
||||
syncPreview();
|
||||
syncWhen();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user