← back to Rentv 2026
public/admin/pr-intelligence/deal-desk.html
127 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRE PR Intelligence — Deal Desk</title>
<link rel="stylesheet" href="/admin/pr-intelligence/pr-shared.css">
<style>
.dd-wrap{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start}
@media(max-width:960px){.dd-wrap{grid-template-columns:1fr}}
.dd-card{border:1px solid var(--line,#e4e7ea);border-radius:10px;background:var(--card,#fff);padding:16px 18px;margin:0 0 16px}
.dd-card h3{margin:0 0 4px;font-size:14px} .dd-card .sub{font-size:12px;color:var(--muted,#5b636b);margin:0 0 10px}
.dd-field{margin:0 0 10px} .dd-field label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted,#5b636b);margin:0 0 3px}
.dd-field input,.dd-field select,.dd-field textarea{width:100%;box-sizing:border-box;font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--line,#d7dde3);border-radius:8px;background:#fff}
.dd-field textarea{min-height:80px;resize:vertical}
.dd-two{display:flex;gap:10px}.dd-two>*{flex:1}
.dd-cover{font-size:12px;border-radius:6px;padding:6px 10px;margin:0 0 10px}
.dd-cover.ok{background:#e7f6ec;color:#1c7a3e} .dd-cover.bad{background:#fff2e0;color:#8a5a00}
.dd-chat{max-height:220px;overflow:auto;border:1px solid var(--line,#eee);border-radius:8px;padding:8px 10px;margin:0 0 8px;background:var(--wash,#f7f8fa)}
.dd-msg{font-size:13px;margin:4px 0;line-height:1.4} .dd-msg.u{text-align:right} .dd-msg.u b{background:#0a66c2;color:#fff;padding:3px 8px;border-radius:8px;display:inline-block} .dd-msg.a b{background:#eef1f4;padding:3px 8px;border-radius:8px;display:inline-block;font-weight:500}
.dd-social textarea{min-height:90px} .cs-cost{display:inline-block;background:#e7f6ec;color:#1c7a3e;border-radius:6px;padding:2px 8px;font-size:11px;font-weight:700}
.cc{font-size:11px;color:var(--muted,#5b636b);margin-top:4px}
.spin{display:inline-block;width:13px;height:13px;border:2px solid #c9d3dd;border-top-color:#0a6cff;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px}@keyframes sp{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<script src="/admin/pr-intelligence/pr-shared.js"></script>
<script>
(async function () {
const c = PR.frame('deal-desk', 'Deal Desk',
'Adjust any field by typing an instruction, then spin social from it — the local model ($0) handles edits + social. “Pull a RENTV closing” surfaces a real past RENTV deal from the corpus to repurpose ($0); for a deal RENTV hasn’t covered yet, use the Gemini engine (needs credits).');
c.insertAdjacentHTML('beforeend', '<div class="dd-wrap"><div id="left"></div><div id="right"></div></div>');
const $ = id => document.getElementById(id);
// Seeded with a REAL uncovered deal (verified not in RENTV's news archive), sourced 2026-08.
let deal = {
headline: "BlackRock Buys Camden's 11-Property SoCal Apartment Portfolio for $1.63B",
txn_type: "Sale", property_type: "Multifamily",
address: "11-property portfolio", city: "LA / Orange / Riverside / San Diego counties", state: "CA",
price: 1630000000, price_label: "$1.63B", units: 3620, sqft: 0,
buyer: "A BlackRock-managed fund", seller: "Camden Property Trust", broker: "JLL Capital Markets",
close_date: "2026-08-05",
summary: "A BlackRock-managed fund acquired Camden Property Trust's 11-property, 3,620-unit Southern California apartment portfolio for $1.63 billion — the largest U.S. multifamily sale since June 2024. The 96%-occupied portfolio spans Los Angeles, Orange, Riverside and San Diego counties; JLL Capital Markets represented Camden and arranged $566.6 million in agency acquisition financing covering seven of the properties.",
};
let coveredNote = null; // seeded deal is verified uncovered
const F = [['headline', 'Headline', 'text'], ['txn_type', 'Transaction', 'sel:Sale,Financing,Lease'], ['property_type', 'Property type', 'sel:Multifamily,Office,Industrial,Retail,Hotel,Mixed-Use,Land'],
['address', 'Address', 'text'], ['city', 'City / area', 'text'], ['state', 'State', 'text'],
['price_label', 'Price', 'text'], ['units', 'Units', 'num'], ['sqft', 'Sq ft', 'num'],
['buyer', 'Buyer', 'text'], ['seller', 'Seller', 'text'], ['broker', 'Broker', 'text'], ['close_date', 'Close date', 'text'],
['summary', 'Summary', 'area']];
function renderDeal() {
$('left').innerHTML = `
<div class="dd-card">
<div style="display:flex;justify-content:space-between;align-items:center"><h3>The deal</h3><button class="btn sm" id="find" title="Pulls a real past RENTV closing from the corpus to repurpose into social ($0). For a deal RENTV hasn't covered, top up Gemini (engine=gemini).">🔎 Pull a RENTV closing (repurpose)</button></div>
<div class="cc" id="findmeta"></div>
${coveredNote ? `<div class="dd-cover bad">⚠ RENTV may already cover this: “${PR.esc(coveredNote.title)}”</div>` : `<div class="dd-cover ok">✅ Verified: not in RENTV’s news archive (uncovered).</div>`}
${F.map(([k, label, type]) => {
if (type === 'area') return `<div class="dd-field"><label>${label}</label><textarea data-k="${k}">${PR.esc(deal[k] || '')}</textarea></div>`;
if (type && type.startsWith('sel:')) return `<div class="dd-field"><label>${label}</label><select data-k="${k}">${type.slice(4).split(',').map(o => `<option ${o === deal[k] ? 'selected' : ''}>${o}</option>`).join('')}</select></div>`;
return `<div class="dd-field"><label>${label}</label><input data-k="${k}" ${type === 'num' ? 'type="number"' : ''} value="${PR.esc(deal[k] == null ? '' : deal[k])}"></div>`;
}).join('')}
</div>`;
// field edits write straight back to the deal object
$('left').querySelectorAll('[data-k]').forEach(el => el.addEventListener('input', () => { deal[el.dataset.k] = el.type === 'number' ? Number(el.value) : el.value; }));
$('find').addEventListener('click', findAnother);
}
async function findAnother() {
$('findmeta').innerHTML = '<span class="spin"></span> Pulling a real RENTV closing from the corpus to repurpose…';
try {
const j = await PR.api('/../dealdesk/find', { method: 'POST', body: { avoid: deal.headline } });
if (j.deal && j.deal.headline) { deal = j.deal; coveredNote = j.covered; renderDeal(); $('findmeta').innerHTML = `<span class="cs-cost">${PR.esc(j.cost || '')}</span> · sources: ${(j.sources || []).length}`; }
else $('findmeta').innerHTML = '<span class="warn">No deal returned.</span>';
} catch (e) { $('findmeta').innerHTML = `<span class="warn" style="color:#b23a1e">${PR.esc(e.message)}</span> — (corpus find failed; edit the seeded deal via chat instead.)`; }
}
// ── Right: chat-adjust + social ─────────────────────────────────────────────
$('right').innerHTML = `
<div class="dd-card">
<h3>Adjust by chat</h3><div class="sub">Type a plain-English change — "buyer was actually Blackstone", "price is $1.7B", "make the summary punchier". Local model · $0.</div>
<div class="dd-chat" id="chat"><div class="dd-msg a"><b>Tell me what to fix on the deal and I’ll update the fields.</b></div></div>
<div class="dd-two"><input id="msg" placeholder="e.g. the broker was CBRE, not JLL" style="font:inherit;font-size:13px;padding:8px 10px;border:1px solid var(--line,#d7dde3);border-radius:8px"><button class="btn" id="send">Send</button></div>
</div>
<div class="dd-card dd-social">
<div style="display:flex;justify-content:space-between;align-items:center"><h3>Social from this deal</h3><button class="btn" id="gensocial">✨ Generate</button></div>
<div class="cc" id="socmeta"></div>
<div class="dd-field"><label>LinkedIn <a class="cs-copy" data-copy="s_li">📋</a></label><textarea id="s_li"></textarea></div>
<div class="dd-field"><label>X / Twitter <a class="cs-copy" data-copy="s_x">📋</a></label><textarea id="s_x"></textarea></div>
<div class="dd-field"><label>Instagram <a class="cs-copy" data-copy="s_ig">📋</a></label><textarea id="s_ig"></textarea></div>
</div>`;
function addMsg(who, text) { const box = $('chat'); box.insertAdjacentHTML('beforeend', `<div class="dd-msg ${who}"><b>${PR.esc(text)}</b></div>`); box.scrollTop = box.scrollHeight; }
async function sendChat() {
const m = $('msg').value.trim(); if (!m) return;
addMsg('u', m); $('msg').value = ''; $('send').disabled = true; addMsg('a', '…');
try {
const j = await PR.api('/../dealdesk/chat', { method: 'POST', body: { deal, message: m } });
$('chat').lastElementChild.querySelector('b').textContent = j.reply || 'Updated.';
if (j.deal) { deal = j.deal; renderDeal(); }
} catch (e) { $('chat').lastElementChild.querySelector('b').textContent = '⚠ ' + e.message; }
finally { $('send').disabled = false; }
}
$('send').addEventListener('click', sendChat);
$('msg').addEventListener('keydown', e => { if (e.key === 'Enter') sendChat(); });
$('gensocial').addEventListener('click', () => PR.guard(async () => {
$('gensocial').disabled = true; $('socmeta').innerHTML = '<span class="spin"></span> Writing posts from the deal…';
try {
const j = await PR.api('/../dealdesk/social', { method: 'POST', body: { deal } });
$('s_li').value = j.linkedin || ''; $('s_x').value = j.x || ''; $('s_ig').value = j.instagram || '';
$('socmeta').innerHTML = `<span class="cs-cost">${PR.esc(j.cost || '$0')}</span> · posts generated`;
} finally { $('gensocial').disabled = false; }
}, null));
document.addEventListener('click', (e) => {
const a = e.target.closest('.cs-copy'); if (!a) return; e.preventDefault();
const el = $(a.getAttribute('data-copy')); if (!el || !navigator.clipboard) return;
navigator.clipboard.writeText(el.value || '').then(() => { const o = a.textContent; a.textContent = '✓'; setTimeout(() => a.textContent = o, 1200); });
});
renderDeal();
})();
</script>
</body>
</html>