feat(prospector): align design contract with PWA-only architecture
Add the three missing first-class design prototypes and retire the stale
iOS/OSX-SSO framing so designs/ (the authoritative visual+behavior contract)
matches the current single-PWA reality.
- add designs/campaigns.html (facets/preview/launch; core prospecting feature)
- add designs/markets.html (tour-market stats: peak hours/days, conversion)
- add designs/control.html (GO/PAUSE/AWAY kill-switch, digest, activity, held)
- rewrite designs/index.html: PWA-only hub, drop iOS/OSX/SSO, link all 9 designs
- remove designs/ios-prospector-tab.html (Swift target dropped in bcbd558)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bcbd558e9d
commit
0a5af348dd
5 changed files with 887 additions and 310 deletions
327
designs/campaigns.html
Normal file
327
designs/campaigns.html
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quinn Prospector • Campaigns</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--app-font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: var(--app-font);
|
||||
font-feature-settings: "kern" "liga" "tnum";
|
||||
}
|
||||
.mac-window {
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.6), 0 10px 10px -5px rgb(0 0 0 / 0.4);
|
||||
border: 1px solid #3f3f46;
|
||||
}
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-secondary { color: #cbd5e1; }
|
||||
.text-muted { color: #94a3b8; }
|
||||
/* facet chips — mirror web .chip / .chip--active */
|
||||
.chip { background: #27272a; border: 1px solid #3f3f46; border-radius: 9999px; padding: 3px 10px; font-size: 11px; cursor: pointer; transition: background .08s ease, border-color .08s ease; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.chip:hover { background: #3f3f46; }
|
||||
.chip.chip--active { background: #064e3b; border-color: #10b981; color: #6ee7b7; }
|
||||
.chip__count { font-size: 9px; background: #18181b; border-radius: 9999px; padding: 0 5px; color: #94a3b8; }
|
||||
.chip.chip--active .chip__count { background: #022c22; color: #34d399; }
|
||||
.pill { background: #27272a; border: 1px solid #3f3f46; border-radius: 9999px; padding: 2px 9px; font-size: 10px; font-mono: true; }
|
||||
.tag { background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 1px 7px; font-size: 10px; font-family: ui-monospace, monospace; color: #93c5fd; }
|
||||
.modal { display: none; position: fixed; z-index: 100; inset: 0; background: rgba(0,0,0,0.7); }
|
||||
.modal-content { background: #1c1c1f; margin: 12% auto; padding: 20px; border: 1px solid #3f3f46; width: 90%; max-width: 480px; border-radius: 12px; }
|
||||
.input-sm { background: #18181b; border: 1px solid #3f3f46; border-radius: 6px; padding: 4px 8px; font-size: 12px; color: #e2e8f0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0a0a0c] flex items-center justify-center min-h-screen p-6 text-[#e2e8f0]">
|
||||
<div class="mac-window w-[1100px] bg-[#1c1c1f] border border-[#3f3f46] rounded-2xl overflow-hidden shadow-2xl flex flex-col" style="height: 720px;">
|
||||
<!-- macOS Title Bar -->
|
||||
<div class="h-9 bg-[#27272a] border-b border-[#3f3f46] flex items-center px-3 gap-x-2 flex-shrink-0">
|
||||
<div class="flex gap-x-1.5">
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#ff5f57] border border-[#e0443e]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#febc2e] border border-[#d9a023]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#28c840] border border-[#1a9e2b]"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-sm font-medium text-primary">Quinn Prospector — Campaigns</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-2 text-[10px]">
|
||||
<div class="px-2 py-px bg-emerald-900/60 text-emerald-400 rounded text-[9px] font-mono flex items-center gap-x-1">
|
||||
<i class="fa-solid fa-bullhorn fa-xs"></i>
|
||||
<span>core: targeted outreach + auto-replies</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="h-11 bg-[#18181b] border-b border-[#3f3f46] px-4 flex items-center justify-between flex-shrink-0">
|
||||
<div class="flex items-center gap-x-3 text-xs">
|
||||
<a href="index.html" class="text-muted hover:text-white"><i class="fa-solid fa-arrow-left"></i> Hub</a>
|
||||
<span class="text-muted">Audience builder → preview → launch. Replies auto-handled by the runner (GO mode).</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted font-mono" id="facet-total">124 prospects in scope</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- LEFT: audience filter -->
|
||||
<div class="w-[420px] border-r border-[#3f3f46] overflow-auto p-4 bg-[#18181b] space-y-4">
|
||||
<div>
|
||||
<div class="text-xs font-semibold mb-2 uppercase tracking-wide text-zinc-400">Audience filter</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-[10px] text-muted mb-1.5">Bands <span class="text-emerald-400">(Life is opt-in only)</span></div>
|
||||
<div id="facet-segments" class="flex flex-wrap gap-1.5"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-[10px] text-muted mb-1.5">Markets</div>
|
||||
<div id="facet-markets" class="flex flex-wrap gap-1.5"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-[10px] text-muted mb-1.5">Tags (classification)</div>
|
||||
<div id="facet-tags" class="flex flex-wrap gap-1.5"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-semibold mb-2 uppercase tracking-wide text-zinc-400">Time-ago windows (days)</div>
|
||||
<div class="grid grid-cols-[70px_1fr_1fr] gap-2 items-center text-xs">
|
||||
<span class="text-muted">First msg</span>
|
||||
<input class="input-sm" placeholder="≥ min" inputmode="numeric" id="first-min">
|
||||
<input class="input-sm" placeholder="≤ max" inputmode="numeric" id="first-max">
|
||||
<span class="text-muted">Last msg</span>
|
||||
<input class="input-sm" placeholder="≥ min" inputmode="numeric" id="last-min">
|
||||
<input class="input-sm" placeholder="≤ max" inputmode="numeric" id="last-max">
|
||||
</div>
|
||||
<div class="text-[10px] text-muted mt-1.5">e.g. last-msg ≥ 14 days = re-engage quiet prospects; first-msg ≤ 7 = fresh inbound only.</div>
|
||||
</div>
|
||||
|
||||
<button onclick="previewAudience()" class="w-full px-3 py-2 bg-emerald-600 hover:bg-emerald-500 transition text-white rounded-lg text-sm font-medium flex items-center justify-center gap-x-2">
|
||||
<i class="fa-solid fa-users-viewfinder"></i>
|
||||
<span>Preview audience</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: preview + launch + past campaigns -->
|
||||
<div class="flex-1 overflow-auto p-4 space-y-4">
|
||||
<!-- Preview card -->
|
||||
<div id="preview-card" class="bg-zinc-900 border border-zinc-700 rounded-xl p-4 hidden">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="font-semibold text-sm">Audience · <span id="preview-matched" class="text-emerald-400">0</span> matched</div>
|
||||
<span class="text-[10px] text-muted">Filtered live against the roster (same atoms as the 1-view)</span>
|
||||
</div>
|
||||
<div id="preview-pills" class="flex flex-wrap gap-1.5 mb-3"></div>
|
||||
<div class="text-[10px] text-muted mb-1">Sample handles</div>
|
||||
<div id="preview-sample" class="flex flex-wrap gap-1.5 mb-3"></div>
|
||||
|
||||
<div class="border-t border-zinc-700 pt-3">
|
||||
<div class="text-xs font-semibold mb-2 uppercase tracking-wide text-zinc-400">Launch</div>
|
||||
<input id="campaign-name" placeholder="Campaign name" class="w-full input-sm mb-2">
|
||||
<select id="template-key" class="w-full input-sm mb-3">
|
||||
<option value="">Select 🌹 template…</option>
|
||||
<option value="incall-only">incall-only — Williamsburg • $1000/hr • pink hair…</option>
|
||||
<option value="opener-1">opener-1 — Hey, rates? (opener)</option>
|
||||
<option value="reengage">reengage — Still around? new dates this month…</option>
|
||||
<option value="bargain-hold">bargain-hold — Bargain-hunter version + hold the price</option>
|
||||
</select>
|
||||
<button id="launch-btn" onclick="openConfirm()" disabled class="w-full px-3 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-700 disabled:text-zinc-500 transition text-white rounded-lg text-sm font-medium">
|
||||
Launch to <span id="launch-count">0</span>
|
||||
</button>
|
||||
<div id="launch-status" class="text-[10px] text-emerald-400 mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-empty" class="bg-zinc-900/50 border border-dashed border-zinc-700 rounded-xl p-6 text-center text-xs text-muted">
|
||||
Build a filter on the left, then <span class="text-emerald-400">Preview audience</span> to see the matched count, segment/market breakdown, and sample handles before launch.
|
||||
</div>
|
||||
|
||||
<!-- Past campaigns -->
|
||||
<div class="bg-zinc-900 border border-zinc-700 rounded-xl overflow-hidden">
|
||||
<div class="px-4 py-2 bg-[#27272a] text-sm font-semibold flex items-center justify-between">
|
||||
<span>Past campaigns</span>
|
||||
<span class="text-[10px] text-muted">enqueued / skipped / matched · auto-replies tracked in Control → Activity</span>
|
||||
</div>
|
||||
<div id="past-campaigns" class="divide-y divide-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom status -->
|
||||
<div class="h-6 bg-[#18181b] border-t border-[#3f3f46] px-4 flex items-center text-[10px] text-muted font-mono flex-shrink-0">
|
||||
Launching enqueues to the runner outbox (cap-aware). Inbound replies are auto-qualified + drafted per RUNNER-POLICY (GO). Held → Control view.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm modal -->
|
||||
<div id="confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="flex justify-between mb-2">
|
||||
<h3 class="font-semibold">Confirm launch</h3>
|
||||
<span onclick="closeConfirm()" class="cursor-pointer text-xl leading-none">×</span>
|
||||
</div>
|
||||
<div id="confirm-body" class="text-xs text-secondary space-y-2"></div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button onclick="closeConfirm()" class="px-3 py-1.5 text-xs bg-zinc-700 hover:bg-zinc-600 rounded">Cancel</button>
|
||||
<button onclick="confirmLaunch()" class="px-3 py-1.5 text-xs bg-emerald-600 hover:bg-emerald-500 text-white rounded">Send real messages</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Facet counts mirror GET /prospector/campaigns/facets
|
||||
const facets = {
|
||||
segments: [
|
||||
{ key: 'dates', count: 82 },
|
||||
{ key: 'digital', count: 31 },
|
||||
{ key: 'life', count: 11, optIn: true },
|
||||
],
|
||||
markets: [
|
||||
{ key: 'NYC', count: 71 },
|
||||
{ key: 'LA', count: 28 },
|
||||
{ key: 'SF', count: 15 },
|
||||
{ key: 'CHI', count: 10 },
|
||||
],
|
||||
classifications: [
|
||||
{ key: 'qualified', count: 44 },
|
||||
{ key: 'content-curious', count: 22 },
|
||||
{ key: 'work:dates', count: 18 },
|
||||
{ key: 'harvester', count: 9 },
|
||||
],
|
||||
total: 124,
|
||||
};
|
||||
|
||||
const sel = { segments: new Set(), markets: new Set(), tags: new Set() };
|
||||
|
||||
const pastCampaigns = [
|
||||
{ id: 'c1', name: 'NYC re-engage (Jun)', template_key: 'reengage', matched_count: 41, enqueued_count: 38, skipped_count: 3, created_at: '2026-06-22T14:00:00Z' },
|
||||
{ id: 'c2', name: 'Fresh dates openers', template_key: 'opener-1', matched_count: 19, enqueued_count: 19, skipped_count: 0, created_at: '2026-06-18T09:30:00Z' },
|
||||
];
|
||||
|
||||
function renderFacetGroup(containerId, group, bucket) {
|
||||
const el = document.getElementById(containerId);
|
||||
el.innerHTML = '';
|
||||
group.forEach((o) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'chip' + (sel[bucket].has(o.key) ? ' chip--active' : '');
|
||||
b.innerHTML = `${o.key}${o.optIn ? ' <i class="fa-solid fa-lock fa-2xs text-emerald-400"></i>' : ''} <span class="chip__count">${o.count}</span>`;
|
||||
b.onclick = () => {
|
||||
if (sel[bucket].has(o.key)) sel[bucket].delete(o.key);
|
||||
else sel[bucket].add(o.key);
|
||||
renderFacets();
|
||||
};
|
||||
el.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFacets() {
|
||||
renderFacetGroup('facet-segments', facets.segments, 'segments');
|
||||
renderFacetGroup('facet-markets', facets.markets, 'markets');
|
||||
renderFacetGroup('facet-tags', facets.classifications, 'tags');
|
||||
}
|
||||
|
||||
function buildFilter() {
|
||||
return {
|
||||
segments: [...sel.segments],
|
||||
markets: [...sel.markets],
|
||||
classifications: [...sel.tags],
|
||||
firstMsgAgo: { min: document.getElementById('first-min').value, max: document.getElementById('first-max').value },
|
||||
lastMsgAgo: { min: document.getElementById('last-min').value, max: document.getElementById('last-max').value },
|
||||
};
|
||||
}
|
||||
|
||||
function previewAudience() {
|
||||
// Simulate POST /campaigns/preview — narrow by selected facets
|
||||
const segs = sel.segments.size ? [...sel.segments] : facets.segments.map((s) => s.key);
|
||||
const mkts = sel.markets.size ? [...sel.markets] : facets.markets.map((m) => m.key);
|
||||
|
||||
const bySegment = facets.segments.filter((s) => segs.includes(s.key)).map((s) => ({ key: s.key, count: Math.round(s.count * 0.7) }));
|
||||
const byMarket = facets.markets.filter((m) => mkts.includes(m.key)).map((m) => ({ key: m.key, count: Math.round(m.count * 0.6) }));
|
||||
const matched = bySegment.reduce((a, r) => a + r.count, 0);
|
||||
|
||||
document.getElementById('preview-empty').classList.add('hidden');
|
||||
const card = document.getElementById('preview-card');
|
||||
card.classList.remove('hidden');
|
||||
document.getElementById('preview-matched').textContent = matched;
|
||||
document.getElementById('launch-count').textContent = matched;
|
||||
|
||||
const pills = document.getElementById('preview-pills');
|
||||
pills.innerHTML = '';
|
||||
bySegment.forEach((r) => { const s = document.createElement('span'); s.className = 'pill'; s.textContent = `${r.key}: ${r.count}`; pills.appendChild(s); });
|
||||
byMarket.forEach((r) => { const s = document.createElement('span'); s.className = 'pill'; s.textContent = `${r.key}: ${r.count}`; pills.appendChild(s); });
|
||||
|
||||
const sampleHandles = ['+14125551234', '+14125555678', '+13125551234', '+521234567890', '+14155559012'];
|
||||
const sample = document.getElementById('preview-sample');
|
||||
sample.innerHTML = '';
|
||||
sampleHandles.slice(0, Math.min(5, matched)).forEach((h) => { const s = document.createElement('span'); s.className = 'tag'; s.textContent = h; sample.appendChild(s); });
|
||||
if (matched > 5) { const s = document.createElement('span'); s.className = 'text-[10px] text-muted self-center'; s.textContent = `+${matched - 5} more`; sample.appendChild(s); }
|
||||
|
||||
validateLaunch();
|
||||
}
|
||||
|
||||
function validateLaunch() {
|
||||
const name = document.getElementById('campaign-name').value.trim();
|
||||
const tpl = document.getElementById('template-key').value;
|
||||
const matched = parseInt(document.getElementById('preview-matched').textContent, 10) || 0;
|
||||
document.getElementById('launch-btn').disabled = !(name && tpl && matched > 0);
|
||||
}
|
||||
|
||||
function openConfirm() {
|
||||
const name = document.getElementById('campaign-name').value.trim();
|
||||
const tpl = document.getElementById('template-key').value;
|
||||
const matched = document.getElementById('preview-matched').textContent;
|
||||
document.getElementById('confirm-body').innerHTML =
|
||||
`Launch <strong>"${name}"</strong> to <strong>${matched}</strong> prospects with template <span class="tag">${tpl}</span>?<br><br>This sends <strong class="text-orange-400">real messages</strong> via the runner outbox (cap-aware). Skips are deduped against recent sends and held prospects.`;
|
||||
document.getElementById('confirm-modal').style.display = 'block';
|
||||
}
|
||||
function closeConfirm() { document.getElementById('confirm-modal').style.display = 'none'; }
|
||||
|
||||
function confirmLaunch() {
|
||||
const name = document.getElementById('campaign-name').value.trim();
|
||||
const tpl = document.getElementById('template-key').value;
|
||||
const matched = parseInt(document.getElementById('preview-matched').textContent, 10) || 0;
|
||||
const skipped = Math.min(matched, Math.round(matched * 0.08));
|
||||
const enqueued = matched - skipped;
|
||||
closeConfirm();
|
||||
document.getElementById('launch-status').textContent = `Launched: ${enqueued} enqueued, ${skipped} skipped of ${matched} matched. Replies will auto-handle in GO mode.`;
|
||||
pastCampaigns.unshift({ id: 'c' + Date.now(), name, template_key: tpl, matched_count: matched, enqueued_count: enqueued, skipped_count: skipped, created_at: new Date().toISOString() });
|
||||
renderPast();
|
||||
document.getElementById('campaign-name').value = '';
|
||||
document.getElementById('template-key').value = '';
|
||||
validateLaunch();
|
||||
}
|
||||
|
||||
function renderPast() {
|
||||
const el = document.getElementById('past-campaigns');
|
||||
el.innerHTML = '';
|
||||
if (pastCampaigns.length === 0) { el.innerHTML = '<div class="px-4 py-3 text-xs text-muted">None yet.</div>'; return; }
|
||||
pastCampaigns.forEach((c) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'px-4 py-2.5';
|
||||
const when = new Date(c.created_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
d.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm">${c.name}</span>
|
||||
<span class="text-[10px] text-muted">${when}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="tag">${c.template_key}</span>
|
||||
<span class="text-[10px] text-muted">${c.enqueued_count} sent · ${c.skipped_count} skipped · ${c.matched_count} matched</span>
|
||||
</div>`;
|
||||
el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('input', (e) => {
|
||||
if (['campaign-name', 'template-key'].includes(e.target.id)) validateLaunch();
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
renderFacets();
|
||||
renderPast();
|
||||
document.getElementById('facet-total').textContent = `${facets.total} prospects in scope`;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
222
designs/control.html
Normal file
222
designs/control.html
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quinn Prospector • Control</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--app-font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: var(--app-font);
|
||||
font-feature-settings: "kern" "liga" "tnum";
|
||||
}
|
||||
.mac-window {
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.6), 0 10px 10px -5px rgb(0 0 0 / 0.4);
|
||||
border: 1px solid #3f3f46;
|
||||
}
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-muted { color: #94a3b8; }
|
||||
.panel { background: #18181b; border: 1px solid #3f3f46; border-radius: 12px; padding: 14px; }
|
||||
.panel__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.panel__title { font-size: 13px; font-weight: 600; color: #f1f5f9; }
|
||||
.panel__meta { font-size: 10px; color: #64748b; font-family: ui-monospace, monospace; }
|
||||
/* mode kill-switch — mirror web .mode-seg / .mode-btn */
|
||||
.mode-seg { display: flex; gap: 8px; }
|
||||
.mode-btn { flex: 1; padding: 12px; border-radius: 10px; font-size: 15px; font-weight: 700; background: #27272a; color: #94a3b8; border: 1px solid #3f3f46; cursor: pointer; transition: all .1s ease; }
|
||||
.mode-btn:hover { background: #3f3f46; }
|
||||
.mode-btn--active.mode-btn--go { background: #064e3b; border-color: #10b981; color: #6ee7b7; }
|
||||
.mode-btn--active.mode-btn--pause { background: #78350f; border-color: #f59e0b; color: #fcd34d; }
|
||||
.mode-btn--active.mode-btn--away { background: #1e3a5f; border-color: #3b82f6; color: #93c5fd; }
|
||||
/* digest pills */
|
||||
.pills { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.pill { font-size: 11px; padding: 3px 10px; border-radius: 9999px; border: 1px solid #3f3f46; background: #27272a; }
|
||||
.pill--send { background: #064e3b; border-color: #10b981; color: #6ee7b7; }
|
||||
.pill--hold { background: #78350f; border-color: #f59e0b; color: #fcd34d; }
|
||||
.pill--err { background: #7f1d1d; border-color: #ef4444; color: #fca5a5; }
|
||||
.pill--ok { background: #064e3b; border-color: #10b981; color: #6ee7b7; }
|
||||
.pill--bad { background: #7f1d1d; border-color: #ef4444; color: #fca5a5; }
|
||||
/* activity terminal */
|
||||
.terminal { background: #0a0a0c; border: 1px solid #27272a; border-radius: 8px; padding: 8px; height: 180px; overflow: auto; font-family: ui-monospace, monospace; }
|
||||
.logrow { display: flex; align-items: center; gap: 8px; font-size: 11px; padding: 2px 0; }
|
||||
.logrow__time { color: #64748b; }
|
||||
.badge { font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 4px; }
|
||||
.badge--send { background: #064e3b; color: #6ee7b7; }
|
||||
.badge--hold { background: #78350f; color: #fcd34d; }
|
||||
.logrow__handle { color: #e2e8f0; font-weight: 500; }
|
||||
.logrow__class { color: #93c5fd; }
|
||||
.logrow__reason { color: #fca5a5; }
|
||||
/* held queue rows */
|
||||
.held { background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; }
|
||||
.held__top { display: flex; justify-content: space-between; }
|
||||
.held__handle { font-weight: 500; font-size: 12px; }
|
||||
.held__time { font-size: 10px; color: #64748b; }
|
||||
.held__meta { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
|
||||
.tag { background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 0 7px; font-size: 10px; color: #93c5fd; }
|
||||
.held__reason { font-size: 11px; color: #fca5a5; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0a0a0c] flex items-center justify-center min-h-screen p-6 text-[#e2e8f0]">
|
||||
<div class="mac-window w-[1100px] bg-[#1c1c1f] border border-[#3f3f46] rounded-2xl overflow-hidden shadow-2xl flex flex-col" style="height: 760px;">
|
||||
<!-- macOS Title Bar -->
|
||||
<div class="h-9 bg-[#27272a] border-b border-[#3f3f46] flex items-center px-3 gap-x-2 flex-shrink-0">
|
||||
<div class="flex gap-x-1.5">
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#ff5f57] border border-[#e0443e]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#febc2e] border border-[#d9a023]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#28c840] border border-[#1a9e2b]"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-sm font-medium text-primary">Quinn Prospector — Control</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-2 text-[10px]">
|
||||
<div id="mode-chip" class="px-2 py-px bg-emerald-900/60 text-emerald-400 rounded text-[9px] font-mono flex items-center gap-x-1">
|
||||
<i class="fa-solid fa-circle fa-2xs"></i><span>runner: GO</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-9 bg-[#18181b] border-b border-[#3f3f46] px-4 flex items-center gap-x-3 text-xs flex-shrink-0">
|
||||
<a href="index.html" class="text-muted hover:text-white"><i class="fa-solid fa-arrow-left"></i> Hub</a>
|
||||
<span class="text-muted">Kill-switch + digest + live activity + held queue. The operator's monitoring cockpit.</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- PWA install card -->
|
||||
<section class="panel col-span-2" style="background:#18181b">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xs font-semibold mb-1">PWA / Chrome App</div>
|
||||
<div class="text-[10px] text-muted">One installable Chrome PWA, served same-origin under <span class="font-mono text-emerald-400">/prospector/*</span>. Install for a standalone window — no URL bar, full F12 devtools. (beforeinstallprompt captured; manifest in public/)</div>
|
||||
</div>
|
||||
<button onclick="installPwa()" class="px-3 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-medium whitespace-nowrap flex items-center gap-x-2">
|
||||
<i class="fa-solid fa-download"></i> Install as Chrome App
|
||||
</button>
|
||||
</div>
|
||||
<div id="install-done" class="text-emerald-400 text-xs mt-2 hidden">✓ Installed — launch from Chrome apps or desktop shortcut.</div>
|
||||
</section>
|
||||
|
||||
<!-- Mode control / kill-switch -->
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">Runner Mode</span>
|
||||
<span class="panel__meta" id="mode-updated">updated just now</span>
|
||||
</div>
|
||||
<div class="mode-seg" id="mode-seg">
|
||||
<button class="mode-btn mode-btn--active mode-btn--go" data-mode="GO" onclick="setMode('GO')">GO</button>
|
||||
<button class="mode-btn" data-mode="PAUSE" onclick="setMode('PAUSE')">PAUSE</button>
|
||||
<button class="mode-btn" data-mode="AWAY" onclick="setMode('AWAY')">AWAY</button>
|
||||
</div>
|
||||
<div class="text-xs text-muted mt-3">
|
||||
Current: <strong id="mode-current" class="text-emerald-400">GO</strong> · engine <span class="font-mono">deepseek-r1-distill</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-muted mt-1">GO = auto-qualify + auto-send (cap-aware). PAUSE = drafts only, no sends. AWAY = everything held for review.</div>
|
||||
</section>
|
||||
|
||||
<!-- Digest -->
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">Digest</span>
|
||||
<span class="panel__meta">last 12h</span>
|
||||
</div>
|
||||
<div class="pills">
|
||||
<span class="pill pill--send">sent 14</span>
|
||||
<span class="pill pill--hold">held 3</span>
|
||||
<span class="pill pill--err">errors 0</span>
|
||||
<span class="pill pill--ok">Mac reachable: yes</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-muted mt-3">Rolling 12h digest from the runner. Mac reachability = mac-sync mesh up (Apple Notes pastebin, outbox, messages).</div>
|
||||
</section>
|
||||
|
||||
<!-- Activity feed -->
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">Activity Feed</span>
|
||||
<span class="panel__meta" id="activity-count">6 recent</span>
|
||||
</div>
|
||||
<div class="terminal" id="terminal"></div>
|
||||
</section>
|
||||
|
||||
<!-- Held queue -->
|
||||
<section class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">Held Queue</span>
|
||||
<span class="panel__meta" id="held-count">3 held</span>
|
||||
</div>
|
||||
<div id="held-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-6 bg-[#18181b] border-t border-[#3f3f46] px-4 flex items-center text-[10px] text-muted font-mono flex-shrink-0">
|
||||
Settings + activity + held-queue poll /prospector/* same-origin. Mode change = PUT /prospector/settings.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const activity = [
|
||||
{ id: 'a1', time: '14:31', outcome: 'send', handle: '+14125551234', classification: 'qualified', holdReason: null },
|
||||
{ id: 'a2', time: '14:29', outcome: 'hold', handle: '+13125551234', classification: 'content-curious', holdReason: 'ambiguous intent' },
|
||||
{ id: 'a3', time: '14:22', outcome: 'send', handle: '+521234567890', classification: 'work:dates', holdReason: null },
|
||||
{ id: 'a4', time: '14:18', outcome: 'send', handle: '+14125555678', classification: 'qualified', holdReason: null },
|
||||
{ id: 'a5', time: '14:10', outcome: 'hold', handle: '+12125554321', classification: 'harvester', holdReason: 'low MR / safety screen' },
|
||||
{ id: 'a6', time: '14:02', outcome: 'send', handle: '+14155559012', classification: 'qualified', holdReason: null },
|
||||
];
|
||||
const held = [
|
||||
{ id: 'h1', handle: '+13125551234', time: '14:29', classification: 'content-curious', holdReason: 'ambiguous intent — human review' },
|
||||
{ id: 'h2', handle: '+12125554321', time: '14:10', classification: 'harvester', holdReason: 'low MR / safety screen' },
|
||||
{ id: 'h3', handle: '+14085550001', time: '13:51', classification: null, holdReason: 'AWAY window — all inbound held' },
|
||||
];
|
||||
|
||||
const modeChipClass = { GO: 'bg-emerald-900/60 text-emerald-400', PAUSE: 'bg-amber-900/60 text-amber-400', AWAY: 'bg-blue-900/60 text-blue-400' };
|
||||
|
||||
function setMode(next) {
|
||||
if ((next === 'GO' || next === 'AWAY') && !confirm(`Switch runner to ${next}?`)) return;
|
||||
document.querySelectorAll('#mode-seg .mode-btn').forEach((b) => {
|
||||
const on = b.dataset.mode === next;
|
||||
b.className = 'mode-btn' + (on ? ` mode-btn--active mode-btn--${next.toLowerCase()}` : '');
|
||||
});
|
||||
const cur = document.getElementById('mode-current');
|
||||
cur.textContent = next;
|
||||
cur.className = next === 'GO' ? 'text-emerald-400' : next === 'PAUSE' ? 'text-amber-400' : 'text-blue-400';
|
||||
document.getElementById('mode-updated').textContent = 'updated just now';
|
||||
const chip = document.getElementById('mode-chip');
|
||||
chip.className = `px-2 py-px ${modeChipClass[next]} rounded text-[9px] font-mono flex items-center gap-x-1`;
|
||||
chip.innerHTML = `<i class="fa-solid fa-circle fa-2xs"></i><span>runner: ${next}</span>`;
|
||||
}
|
||||
|
||||
function installPwa() {
|
||||
alert('Use Chrome menu (⋮) → Install Prospector, or "Create shortcut" → "Open as window" for a standalone PWA (full devtools, no chrome). Matches the designs/ PWA spec.');
|
||||
document.getElementById('install-done').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function renderActivity() {
|
||||
const t = document.getElementById('terminal');
|
||||
t.innerHTML = activity.map((i) => `
|
||||
<div class="logrow">
|
||||
<span class="logrow__time">${i.time}</span>
|
||||
<span class="badge badge--${i.outcome}">${i.outcome}</span>
|
||||
<span class="logrow__handle">${i.handle}</span>
|
||||
${i.classification ? `<span class="logrow__class">${i.classification}</span>` : ''}
|
||||
${i.holdReason ? `<span class="logrow__reason">${i.holdReason}</span>` : ''}
|
||||
</div>`).join('');
|
||||
document.getElementById('activity-count').textContent = `${activity.length} recent`;
|
||||
}
|
||||
|
||||
function renderHeld() {
|
||||
const el = document.getElementById('held-list');
|
||||
el.innerHTML = held.map((i) => `
|
||||
<div class="held">
|
||||
<div class="held__top"><span class="held__handle">${i.handle}</span><span class="held__time">${i.time}</span></div>
|
||||
<div class="held__meta">${i.classification ? `<span class="tag">${i.classification}</span>` : ''}<span class="held__reason">${i.holdReason}</span></div>
|
||||
</div>`).join('');
|
||||
document.getElementById('held-count').textContent = `${held.length} held`;
|
||||
}
|
||||
|
||||
window.onload = function () { renderActivity(); renderHeld(); };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,16 +3,15 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quinn Prospector — Design System (iOS + OSX)</title>
|
||||
<title>Quinn Prospector — Design Hub (PWA)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=SF+Mono:wght@400;500&display=swap');
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
||||
:root {
|
||||
--app-font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
}
|
||||
body {
|
||||
body {
|
||||
font-family: var(--app-font);
|
||||
font-feature-settings: "kern" "liga" "tnum";
|
||||
background: #0a0a0c;
|
||||
|
|
@ -23,241 +22,139 @@
|
|||
border: 1px solid #3f3f46;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.6), 0 10px 10px -5px rgb(0 0 0 / 0.4);
|
||||
}
|
||||
.section-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.section-card:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #4b5563;
|
||||
}
|
||||
.drill-link {
|
||||
transition: color 0.1s;
|
||||
}
|
||||
.drill-link:hover {
|
||||
color: #34d399;
|
||||
}
|
||||
.app-card {
|
||||
border: 1px solid #3f3f46;
|
||||
}
|
||||
.app-card:hover {
|
||||
border-color: #10b981;
|
||||
}
|
||||
.nav-pill {
|
||||
background: #18181b;
|
||||
border: 1px solid #3f3f46;
|
||||
}
|
||||
.design-card { transition: all 0.15s ease; border: 1px solid #3f3f46; }
|
||||
.design-card:hover { transform: translateY(-2px); border-color: #10b981; }
|
||||
.text-muted { color: #94a3b8; }
|
||||
.pipe { color: #475569; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-8 max-w-[1280px] mx-auto">
|
||||
<body class="p-8 max-w-[1180px] mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-11 h-11 bg-emerald-500 rounded-2xl flex items-center justify-center ring-1 ring-emerald-400/40">
|
||||
<i class="fa-solid fa-user-check text-white text-3xl"></i>
|
||||
<i class="fa-solid fa-user-check text-white text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-4xl font-semibold tracking-tighter">Quinn Prospector</h1>
|
||||
<p class="text-sm text-muted -mt-1">Design System • 2 Apps (iOS + OSX) • Shared Private quinn-api (SSO)</p>
|
||||
<p class="text-sm text-muted -mt-1">Design Hub • One installable Chrome PWA • served same-origin under <span class="font-mono text-emerald-400">/prospector/*</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-xs space-y-0.5">
|
||||
<div class="px-3 py-1 bg-zinc-900 rounded-full border border-zinc-700 inline-flex items-center gap-x-2">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
|
||||
<span class="font-medium">v0.1 • DRY in @applications/@prospector/@packages (prospector-client + prospector-ui)</span>
|
||||
<span class="font-medium">PWA-only • NestJS + Postgres + MCP</span>
|
||||
</div>
|
||||
<div class="text-muted">Reuses tv-anarchy patterns • Bilingual (OCR ES→EN) support</div>
|
||||
<div class="text-muted">designs/*.html are the authoritative visual + behavior contract</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Architecture (quick visual) -->
|
||||
<!-- System Architecture -->
|
||||
<div class="mb-8 p-5 bg-zinc-900 border border-zinc-700 rounded-2xl">
|
||||
<div class="uppercase tracking-[1px] text-[10px] font-semibold text-emerald-400 mb-3">System Architecture — 2 Apps + Shared Backend</div>
|
||||
|
||||
<div class="uppercase tracking-[1px] text-[10px] font-semibold text-emerald-400 mb-3">System Architecture — One App, One Backend, One Database</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 items-stretch">
|
||||
<!-- iOS -->
|
||||
<div class="flex-1 p-4 bg-zinc-950 border border-blue-700/50 rounded-xl">
|
||||
<div class="font-semibold text-blue-400 flex items-center gap-x-2 mb-1">
|
||||
<i class="fa-solid fa-mobile-screen"></i>
|
||||
<span>iOS App / Tab</span>
|
||||
<!-- PWA -->
|
||||
<div class="flex-1 p-4 bg-zinc-950 border border-emerald-700/50 rounded-xl">
|
||||
<div class="font-semibold text-emerald-400 flex items-center gap-x-2 mb-1">
|
||||
<i class="fa-brands fa-chrome"></i>
|
||||
<span>Installable Chrome PWA (web/)</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted mb-2">Primary tab (tag 3 "Prospector") inside @cocottetech v4 client (ios-fe TabView + ProspectorView.swift in cockpit-kit). Supports the 1-view + Bilingual flag.</div>
|
||||
<a href="ios-prospector-tab.html" class="text-xs text-emerald-400 underline hover:text-emerald-300">Open iOS Tab Design →</a>
|
||||
<div class="mt-1 text-[10px] text-muted">Live impl extracted from LP tracker into cocottetech paths.</div>
|
||||
<div class="text-xs text-muted">The sole operator surface. React PWA installs as a standalone macOS/Chrome window (no URL bar, full devtools). Served same-origin by the backend under <span class="font-mono text-emerald-400">/prospector/*</span>. No Swift app, no native macOS target, no platform <span class="font-mono">my/</span> round-trip.</div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted flex items-center justify-center text-lg">↔ SSO (no mesh/VPN)</div>
|
||||
<div class="text-muted flex items-center justify-center text-lg">↔ same-origin</div>
|
||||
|
||||
<!-- Backend -->
|
||||
<div class="flex-1 p-4 bg-zinc-950 border border-emerald-700/50 rounded-xl">
|
||||
<div class="font-semibold text-emerald-400 mb-1">Shared Backend (Private quinn-api + DB)</div>
|
||||
<div class="text-xs text-muted space-y-0.5 mb-2">
|
||||
• quinn-api (SSO-protected, private access)<br>
|
||||
• Auto-Runner service (local-model autosend per handoff: poll macsync, scam/qualification, fixed templates or LOCAL MODEL per RUNNER-POLICY)<br>
|
||||
• DO GPU on-demand (raw H100 + vLLM + model-boss for uncensored OSS models optimized for prospect work)<br>
|
||||
• macsync for threads, pastebin (🌹 Note), outbox sends, calls, mark-read
|
||||
<div class="font-semibold text-emerald-400 mb-1"><i class="fa-solid fa-server mr-1"></i> NestJS backend (src/) + Postgres</div>
|
||||
<div class="text-xs text-muted space-y-0.5">
|
||||
• Feature-sliced NestJS modules on their own Postgres DB<br>
|
||||
• Auto-runner: poll inbound, scam/safety screen, qualify, draft/send (cap-aware) per RUNNER-POLICY<br>
|
||||
• DO GPU on-demand for classify/draft models<br>
|
||||
• <span class="font-mono">mac-sync</span> mesh: Apple Notes pastebin (🌹), outbox, messages, calendar
|
||||
</div>
|
||||
<a href="hosts-do-gpu.html" class="text-xs text-emerald-400 underline hover:text-emerald-300">View Backend DO GPU Fleet (models only) →</a>
|
||||
</div>
|
||||
|
||||
<div class="text-muted flex items-center justify-center text-lg">↔ SSO</div>
|
||||
<div class="text-muted flex items-center justify-center text-lg">↔ MCP</div>
|
||||
|
||||
<!-- OSX -->
|
||||
<div class="flex-1 p-4 bg-zinc-950 border border-emerald-700/50 rounded-xl">
|
||||
<div class="font-semibold text-emerald-400 flex items-center gap-x-2 mb-1">
|
||||
<i class="fa-solid fa-desktop"></i>
|
||||
<span>OSX App</span>
|
||||
<!-- MCP -->
|
||||
<div class="flex-1 p-4 bg-zinc-950 border border-blue-700/50 rounded-xl">
|
||||
<div class="font-semibold text-blue-400 flex items-center gap-x-2 mb-1">
|
||||
<i class="fa-solid fa-plug"></i>
|
||||
<span>MCP server (@packages/mcp-prospector)</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted mb-2">Dedicated rich desktop companion @applications/@quinn-prospector. Full 1-view, pastebin editor, reports, operator tools. Bilingual flag + drillable reports.</div>
|
||||
<a href="main-view.html" class="text-xs text-emerald-400 underline hover:text-emerald-300">Open OSX Main View →</a>
|
||||
<div class="text-xs text-muted">How agents / coworkers drive the prospector programmatically. The PWA is the operator's direct UI; the MCP is the agent's. Both hit the same backend + DB.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-[10px] text-muted flex gap-x-4">
|
||||
<div><span class="font-medium text-emerald-400">Bilingual / OCR:</span> Data models + UI flag show original (ES etc.) + translated (EN) for non-English inbound (OCR from images or direct).</div>
|
||||
<div><span class="font-medium text-emerald-400">Executor:</span> The auto-runner loop + RUNNER-POLICY (from Executor/prospecting/voice-engine/) drives the local model for live qualifying/booking replies.</div>
|
||||
<div class="mt-3 text-[10px] text-muted flex flex-wrap gap-x-6 gap-y-1">
|
||||
<div><span class="font-medium text-emerald-400">Bilingual / OCR:</span> data models + UI flag show original (ES etc.) + translated (EN) for non-English inbound (OCR from images or direct).</div>
|
||||
<div><span class="font-medium text-emerald-400">Two "market" notions:</span> campaign-targeting market = E.164 calling-code bucket; tour-stop market = metro + date window + timezone.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extraction & Org from LP -->
|
||||
<div class="mb-8 p-4 bg-zinc-950 border border-zinc-700 rounded-xl text-xs">
|
||||
<div class="font-medium text-emerald-400 mb-1">Extract from LP (lilith-platform) & Organize Apps Properly</div>
|
||||
<div class="text-muted">Significant iOS work finished in the feature tracker. Extract the artifacts and organize into the proper @ structure (no more root strays or .project-only living docs).</div>
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 text-[10px]">
|
||||
<div>
|
||||
<strong>iOS side (finished work to extract):</strong><br>
|
||||
• swift-ui/ProspectorMessengerView.swift (the 1-view spec — already ported to cocottetech cockpit-kit)<br>
|
||||
• scripts/ (build.sh, deploy-iphone-wireless.sh, deploy-iwatch-wireless.sh, full-cycle.sh) + ios-tools.config.json + scripts/README.md (DX; active now in cocottetech ios-fe, these are reference)<br>
|
||||
• objectives/README.md + README.md (updated with path correction, status for UI/classification)<br>
|
||||
• quinn-my-ui/ProspectorMessenger.tsx + my/frontend prospector pages (web reports: ProspectorPage with stream/tour/engine tabs + FunnelStrip, BackfillBar, DraftEngineSelector, ExperimentsTab, TourBoardTab etc. — the "4 reports" surfaces)
|
||||
</div>
|
||||
<div>
|
||||
<strong>Organized structure:</strong><br>
|
||||
• @applications/@quinn-prospector (this OSX dedicated app + designs/)<br>
|
||||
• iOS: integrated as tab in @cocottetech (current home) or extract to @applications/@quinn-prospector-ios if dedicated Quinn iOS app needed<br>
|
||||
• Shared: @applications/@prospector/@packages/prospector-client (merged domain+api), prospector-ui (components)<br>
|
||||
• Tracker (.project/feature_quinn-prospector-ios in LP worktree + main): now correctly points to cocottetech paths + marks quinn-ios as historical/legacy. Keep for planning/history only (per doc normalization: live in source docs/, not .project/).
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-emerald-400 text-[10px]">See the dedicated live docs we added: cocottetech cockpit-kit/README.md and ios-fe/README.md (full impl, wiring, status, build DX). The Executor/prospecting/ (HANDOFF-prospector-pastebin-queue.md, PROSPECTOR_TRAINING.md, PROSPECTING-NOTES.md, voice-engine/RUNNER-POLICY.md etc.) is the source for the executor instructions/skills/loops + policy for the local model.</div>
|
||||
</div>
|
||||
|
||||
<!-- Drillable Designs -->
|
||||
<h3 class="font-semibold text-lg mb-3">Drillable Design Mocks (open in browser to explore)</h3>
|
||||
<h3 class="font-semibold text-lg mb-3">Drillable Design Mocks <span class="text-sm font-normal text-muted">(open any file directly in a browser to explore)</span></h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
<!-- iOS -->
|
||||
<div class="p-4 border border-blue-700/40 bg-zinc-900 rounded-2xl">
|
||||
<div class="uppercase text-[10px] tracking-widest text-blue-400 mb-1">iOS</div>
|
||||
<a href="ios-prospector-tab.html" class="block text-lg font-medium mb-1 hover:underline">iOS Tab Mock (device frame)</a>
|
||||
<div class="text-xs text-muted">The iOS Tab Mock (device frame) renders the 1-view as it appears in the live cocottetech client TabView (primary tab). Includes Bilingual support for ES/OCR cases. Matches the extracted LP prototype.</div>
|
||||
<div class="mt-1 text-[10px]"><span class="text-muted">Related live:</span> cocottetech ios-fe/App.swift (TabView with --tab 3) + cockpit-kit/ProspectorView.swift</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<a href="main-view.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Roster</div>
|
||||
<div class="text-lg font-medium mb-1">Main View</div>
|
||||
<div class="text-xs text-muted">The 1-view: Life/Dates/Digital channels, prospect list with MR + classification badges, Classify/Request-MR toolbar, funnel stats, bilingual toggle.</div>
|
||||
</a>
|
||||
|
||||
<!-- OSX -->
|
||||
<div class="p-4 border border-emerald-700/40 bg-zinc-900 rounded-2xl">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">OSX</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 text-sm">
|
||||
<div>
|
||||
<a href="main-view.html" class="hover:underline font-medium">Main View</a>
|
||||
<div class="text-xs text-muted">Full 1-view, channels, list with badges, toolbar (Classify, Pastebin refresh, Bilingual toggle), funnel stats.</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="detail-view.html" class="hover:underline font-medium">Detail + Chat</a>
|
||||
<div class="text-xs text-muted">Prospect detail, bilingual message, Mr Number/stage, quick actions, pastebin preview.</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="pastebin-panel.html" class="hover:underline font-medium">Pastebin Editor</a>
|
||||
<div class="text-xs text-muted">Live macsync-synced 🌹 templates (canon from Executor). Edit + apply for drafts.</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="hosts-do-gpu.html" class="hover:underline font-medium">Backend DO GPU</a>
|
||||
<div class="text-xs text-muted">Model-serving fleet (H100, vLLM). For operators — clients use SSO quinn-api only.</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="queued-tasks.html" class="hover:underline font-medium">Queued Tasks</a>
|
||||
<div class="text-xs text-muted">See & manage the 4+ queued (classify/draft/send/backfill). Cancel, prioritize, run on GPU now. Drill to prospects.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="detail-view.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Prospect</div>
|
||||
<div class="text-lg font-medium mb-1">Detail + Chat</div>
|
||||
<div class="text-xs text-muted">Per-prospect thread, bilingual messages, MR-number screening, classification, stage timeline, composer, quick actions + correction loop.</div>
|
||||
</a>
|
||||
|
||||
<!-- Reports -->
|
||||
<div class="mb-8">
|
||||
<a href="reports-dashboard.html" class="block p-5 border border-emerald-600 bg-emerald-950/30 hover:bg-emerald-950/50 rounded-2xl group">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-semibold text-lg">4 Drillable Prospector Reports (see them fully in the dashboard)</div>
|
||||
<div class="text-sm text-emerald-400 mt-0.5">The core subfeatures from the v4 prospecting brief + web prospector cockpit surfaces (stream, tour, engine, experiments). All 4 are fully visible with tables, filters, bilingual, drills in reports-dashboard.html — no high-level labels.</div>
|
||||
</div>
|
||||
<div class="text-right text-emerald-400 group-hover:underline text-sm">Open full interactive dashboard →</div>
|
||||
</div>
|
||||
<a href="campaigns.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Core • Outreach</div>
|
||||
<div class="text-lg font-medium mb-1">Campaigns</div>
|
||||
<div class="text-xs text-muted">Band/market/tag facet chips (Life opt-in), first/last-msg age windows, Preview audience → matched count + breakdown pills + sample handles, launch with confirm, past-campaigns table. Replies auto-handled by the runner.</div>
|
||||
</a>
|
||||
|
||||
<a href="markets.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Tour analytics</div>
|
||||
<div class="text-lg font-medium mb-1">Markets</div>
|
||||
<div class="text-xs text-muted">Tour-market selector + 7d/30d/90d range, peak-hours stacked bars, conversion-by-hour, peak-days, auto-qualify funnel, by-band & by-locality.</div>
|
||||
</a>
|
||||
|
||||
<a href="control.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Cockpit</div>
|
||||
<div class="text-lg font-medium mb-1">Control</div>
|
||||
<div class="text-xs text-muted">Kill-switch GO/PAUSE/AWAY, digest pills, live activity terminal, held queue, PWA install card. The operator's monitoring panel.</div>
|
||||
</a>
|
||||
|
||||
<a href="queued-tasks.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Runner</div>
|
||||
<div class="text-lg font-medium mb-1">Queued Tasks</div>
|
||||
<div class="text-xs text-muted">Manage the auto-runner queue (classify / draft / send / backfill). Tabs, bulk actions, simulate a runner tick, drill to prospects, runner activity log.</div>
|
||||
</a>
|
||||
|
||||
<a href="reports-dashboard.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Reports</div>
|
||||
<div class="text-lg font-medium mb-1">Reports Dashboard</div>
|
||||
<div class="text-xs text-muted">Drillable prospecting reports with filterable tables, bilingual rows, and detail drills.</div>
|
||||
</a>
|
||||
|
||||
<a href="pastebin-panel.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-emerald-400 mb-1">Templates</div>
|
||||
<div class="text-lg font-medium mb-1">Pastebin Editor</div>
|
||||
<div class="text-xs text-muted">The 🌹 Note synced via mac-sync (Apple Notes). The draft canon — edit + apply to open drafts.</div>
|
||||
</a>
|
||||
|
||||
<a href="hosts-do-gpu.html" class="design-card bg-zinc-900 rounded-2xl p-4 block">
|
||||
<div class="uppercase text-[10px] tracking-widest text-blue-400 mb-1">Infra</div>
|
||||
<div class="text-lg font-medium mb-1">Backend DO GPU</div>
|
||||
<div class="text-xs text-muted">Model-serving fleet (H100, vLLM) for on-demand classify/draft. Operator/infra view only.</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Embedded previews in hub so the home page itself delivers the product (4 tasks you can "see + manage", 4 reports with visible rows) -->
|
||||
<div class="mb-8 p-4 border border-zinc-700 bg-zinc-950 rounded-2xl">
|
||||
<div class="uppercase tracking-widest text-[10px] text-emerald-400 mb-2">Embedded in this hub (non-partial: the numbers have actual visible content here)</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Mini 4 tasks preview -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="font-semibold text-sm">4 Tasks Queued (Auto-Runner)</div>
|
||||
<a href="queued-tasks.html" class="text-xs text-emerald-400 underline">Full manager (tabs, bulk, simulate tick, log) →</a>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded text-xs overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="text-[10px] text-muted"><tr><th class="p-1 text-left">ID</th><th>Type</th><th>Prospect</th><th>Prio</th><th></th></tr></thead>
|
||||
<tbody class="text-[10px]">
|
||||
<tr class="border-t border-zinc-800"><td class="p-1 font-mono">T-4821</td><td>Classify (GPU)</td><td>Maria (ES OCR)</td><td class="text-red-400">High</td><td><a href="queued-tasks.html" class="text-emerald-400">manage</a></td></tr>
|
||||
<tr class="border-t border-zinc-800"><td class="p-1 font-mono">T-4820</td><td>Draft (Local Model + RUNNER-POLICY)</td><td>Alex Rivera</td><td>Med</td><td><a href="queued-tasks.html" class="text-emerald-400">manage</a></td></tr>
|
||||
<tr class="border-t border-zinc-800"><td class="p-1 font-mono">T-4819</td><td>Send (macsync outbox)</td><td>Jay</td><td class="text-red-400">High</td><td><a href="queued-tasks.html" class="text-emerald-400">manage</a></td></tr>
|
||||
<tr class="border-t border-zinc-800"><td class="p-1 font-mono">T-4818</td><td>Backfill</td><td>LA Tour Cohort</td><td>Low</td><td><a href="queued-tasks.html" class="text-emerald-400">manage</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="px-2 py-1 bg-zinc-950 text-[9px] text-muted">Click full manager above to cancel/run/bulk/simulate full handoff loop (poll→screen→qual→draft/send per RUNNER-POLICY). 1 ES bilingual task included.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mini reports at a glance -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="font-semibold text-sm">4 Reports at a Glance (actual rows visible)</div>
|
||||
<a href="reports-dashboard.html" class="text-xs text-emerald-400 underline">Full 4-report dashboard with filters/tables →</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-[10px]">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded p-1.5">
|
||||
<div class="font-medium">1. Cross-Provider Graph</div>
|
||||
<div class="text-muted">Alex (95, 3 prov) • Jordan (88) • Maria ES (82) <a href="reports-dashboard.html#report1" class="text-emerald-400">see table</a></div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded p-1.5">
|
||||
<div class="font-medium">2. Warm Intros</div>
|
||||
<div class="text-muted">Sam Patel accepted (2 turns, booked) • Taylor proposed <a href="reports-dashboard.html#report2" class="text-emerald-400">see list</a></div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded p-1.5">
|
||||
<div class="font-medium">3. Auto-Qualify + Draft</div>
|
||||
<div class="text-muted">Alex: conf92 sent • Maria ES: conf84 bilingual draft • 38 today <a href="reports-dashboard.html#report3" class="text-emerald-400">full queue + bilingual toggle</a></div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded p-1.5">
|
||||
<div class="font-medium">4. Internal Marketplace</div>
|
||||
<div class="text-muted">Beauty lead → SF peer (accepted) • Sam routed <a href="reports-dashboard.html#report4" class="text-emerald-400">see matches</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[9px] text-muted mt-2">These previews are real data excerpts from the full interactive designs. The linked pages have the complete management + filterable tables so every claim ("4 tasks", "4 reports") has visible, actionable content.</div>
|
||||
</div>
|
||||
|
||||
<!-- Executor / Auto-Runner (from LP handoff) -->
|
||||
<div class="mb-8 p-4 border border-amber-700/40 bg-amber-950/10 rounded-2xl text-xs">
|
||||
<div class="font-medium text-amber-400 mb-1">Executor / Auto-Runner (the "skills and loops for handling prospecting inbound")</div>
|
||||
<div class="text-muted">Long-running service (poll macsync inbound, scam/safety screen, qualification state, route to fixed templates or LOCAL MODEL per RUNNER-POLICY.md from Executor/prospecting/voice-engine/, persist draft, send via macsync outbox cap-aware). Lives in quinn-api backend (DO GPU for the local model). Infra to build per the gap analysis in the handoff (poll worker, scam screen in-repo, LIVE sender, mode/control, fixed templates, local model endpoint).</div>
|
||||
<div class="mt-2 text-[10px]"><span class="text-amber-400">Key artifact:</span> RUNNER-POLICY.md (Quinn-owned solicitation brain/policy — build session loads it as data only). See the full handoff in LP tracker + Executor/prospecting/ (PROSPECTOR_TRAINING.md, PROSPECTING-NOTES.md, voice-engine/*).</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[10px] text-muted">
|
||||
All designs are static, fully interactive HTML prototypes. Use them as the complete upfront visual spec before extracting the LP artifacts into the organized @applications/@prospector (OSX central + @packages/) + cocottetech paths (iOS tab) or dedicated iOS app, and before implementing the auto-runner infra. Open files directly in browser to drill (click any link/card). Bilingual data models + UI flag are in the shared @applications/@prospector/@packages and reflected in all mocks + live cocottetech ProspectorView. (Note: @quinn-prospector is legacy transitional.)
|
||||
All designs are static, fully interactive HTML prototypes — open files directly in a browser to drill (click any card/link). They are the authoritative visual + behavior contract per <span class="font-mono">docs/PROSPECTOR.md</span>; when UI/behavior disputes arise, the prototypes win. Bilingual (OCR ES→EN) data models + UI flag show original + translated for non-English inbound, and are reflected across the mocks and the live PWA.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quinn Prospector • iOS Tab (in @cocottetech client)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
:root { --app-font: -apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif; }
|
||||
body { font-family: var(--app-font); background: #000; color: #fff; }
|
||||
.ios-frame { width: 390px; height: 844px; background: #000; border-radius: 60px; padding: 12px; box-shadow: 0 0 0 10px #111, 0 0 0 14px #222; position: relative; overflow: hidden; }
|
||||
.ios-screen { background: #0a0a0c; height: 100%; border-radius: 48px; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.ios-status { height: 44px; background: #000; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; font-size: 12px; }
|
||||
.ios-tabbar { height: 83px; background: #111; border-top: 1px solid #333; display: flex; }
|
||||
.ios-tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 10px; }
|
||||
.ios-tab.active { color: #4ade80; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen p-8 bg-black">
|
||||
<div>
|
||||
<div class="text-center mb-4 text-sm text-emerald-400">iOS: Dedicated quinn-prospector experience (or Quinn-branded tab in cocottetech client)</div>
|
||||
|
||||
<div class="ios-frame mx-auto">
|
||||
<div class="ios-screen">
|
||||
<!-- Status bar -->
|
||||
<div class="ios-status text-xs px-5 pt-2">
|
||||
<span>9:41</span>
|
||||
<span class="flex items-center gap-1"><i class="fa-solid fa-signal"></i> <i class="fa-solid fa-wifi"></i> 100%</span>
|
||||
</div>
|
||||
|
||||
<!-- Nav bar -->
|
||||
<div class="px-4 py-2 flex items-center justify-between bg-black/80">
|
||||
<div class="font-semibold">Prospects • Dates</div>
|
||||
<button class="text-xs bg-emerald-600 px-2 py-0.5 rounded">Classify All</button>
|
||||
</div>
|
||||
|
||||
<!-- Segmented -->
|
||||
<div class="px-4 pt-2">
|
||||
<div class="bg-[#27272a] rounded-full p-0.5 flex text-xs">
|
||||
<div class="flex-1 text-center py-1">Life (Personal)</div>
|
||||
<div class="flex-1 text-center py-1 bg-[#3f3f46] rounded-full font-medium">Dates (Bookings)</div>
|
||||
<div class="flex-1 text-center py-1">Digital</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List (matching prototype) -->
|
||||
<div class="flex-1 overflow-auto p-2 text-sm">
|
||||
<div class="px-3 py-2 border-b border-[#3f3f46] flex items-center gap-3">
|
||||
<div class="w-9 h-9 bg-gray-600 rounded-full flex-shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Alex</div>
|
||||
<div class="text-xs text-zinc-400">Hey, rates?</div>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<div class="inline-block px-1.5 py-0.5 bg-emerald-900 text-emerald-400 rounded text-[10px]">95 MR</div>
|
||||
<div class="text-emerald-400 text-[10px]">qualified • New</div>
|
||||
<div class="text-[8px] text-blue-400">Request MR</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 py-2 border-b border-[#3f3f46] flex items-center gap-3">
|
||||
<div class="w-9 h-9 bg-gray-600 rounded-full flex-shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Jay</div>
|
||||
<div class="text-xs text-zinc-400">Can I book a FaceTime show?</div>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<div class="inline-block px-1.5 py-0.5 bg-orange-900 text-orange-400 rounded text-[10px]">72 MR</div>
|
||||
<div class="text-orange-400 text-[10px]">content-curious • OF-live invited</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ES bilingual demo row (matches live ProspectorView.swift + shared domain model) -->
|
||||
<div class="px-3 py-2 border-b border-[#3f3f46] flex items-center gap-3 bg-zinc-950/60">
|
||||
<div class="w-9 h-9 bg-gray-600 rounded-full flex-shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Maria (ES)</div>
|
||||
<div class="text-xs text-zinc-400">Hola, rates for incall?</div>
|
||||
<div class="text-[10px] text-blue-400">→ Hi, rates for incall?</div>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<div class="inline-block px-1.5 py-0.5 bg-emerald-900 text-emerald-400 rounded text-[10px]">82 MR</div>
|
||||
<div class="text-emerald-400 text-[10px]">work:dates • New</div>
|
||||
<div class="text-purple-400 text-[9px]">ES (OCR)</div>
|
||||
<div class="text-[8px] text-blue-400">Request MR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-3 py-1 text-[10px] text-muted">Bilingual flag (in real SwiftUI ProspectorView + toolbar toggle): shows original (ES/OCR from image or direct) + translated EN inline. Toggle off to see EN only for classify/draft flow.</div>
|
||||
<!-- more rows abbreviated -->
|
||||
<div class="px-3 py-2 text-xs text-muted">+ 35 more (Alex 95 qualified, Jay digital, ops medical, hair, beauty...)</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary bar -->
|
||||
<div class="p-2 text-xs bg-zinc-900/80 flex justify-between">
|
||||
<div>Classified: 27</div>
|
||||
<div>High Score: 95</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab bar (showing Prospector as primary) -->
|
||||
<div class="ios-tabbar text-[10px]">
|
||||
<div class="ios-tab"><i class="fa-solid fa-tray-full"></i><div>Drops</div></div>
|
||||
<div class="ios-tab"><i class="fa-solid fa-photo"></i><div>Assets</div></div>
|
||||
<div class="ios-tab"><i class="fa-solid fa-person-3"></i><div>Fleet</div></div>
|
||||
<div class="ios-tab active"><i class="fa-solid fa-person-2"></i><div>Prospector</div></div>
|
||||
<div class="ios-tab"><i class="fa-solid fa-bolt"></i><div>Activity</div></div>
|
||||
<div class="ios-tab"><i class="fa-solid fa-chart-bar"></i><div>Insights</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-muted text-center">iOS tab (tag 3, primary after Fleet). Matches the SwiftUI prototype extracted from LP. In dedicated quinn-prospector iOS or Quinn-branded view in cocottetech client. Data via private quinn-api (SSO). See cocottetech ios-fe/App.swift for the TabView shell and cockpit-kit for the view impl.</div>
|
||||
<div class="mt-2 text-center"><a href="index.html" class="text-emerald-400 text-xs">← Back to full design hub (includes OSX counterpart + drillable reports)</a></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
247
designs/markets.html
Normal file
247
designs/markets.html
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quinn Prospector • Markets</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--app-font: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, system-ui, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: var(--app-font);
|
||||
font-feature-settings: "kern" "liga" "tnum";
|
||||
}
|
||||
.mac-window {
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.6), 0 10px 10px -5px rgb(0 0 0 / 0.4);
|
||||
border: 1px solid #3f3f46;
|
||||
}
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-muted { color: #94a3b8; }
|
||||
.card { background: #18181b; border: 1px solid #3f3f46; border-radius: 12px; }
|
||||
.card__title { font-size: 12px; font-weight: 600; color: #cbd5e1; margin-bottom: 10px; }
|
||||
/* segmented range control — mirror web .seg */
|
||||
.seg { display: inline-flex; background: #27272a; border-radius: 9999px; padding: 2px; }
|
||||
.seg__btn { padding: 3px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; color: #94a3b8; }
|
||||
.seg__btn--active { background: #3f3f46; color: #fff; }
|
||||
/* vertical stacked hour chart — mirror web .vchart */
|
||||
.vchart { display: flex; align-items: flex-end; gap: 3px; height: 130px; }
|
||||
.vchart__col { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; }
|
||||
.vchart__stack { flex: 1; width: 100%; display: flex; flex-direction: column; justify-content: flex-end; }
|
||||
.vchart__seg--hold { background: #3f3f46; }
|
||||
.vchart__seg--send { background: #10b981; }
|
||||
.vchart__col--peak .vchart__seg--hold { background: #475569; }
|
||||
.vchart__label { font-size: 8px; color: #64748b; margin-top: 3px; height: 10px; }
|
||||
/* horizontal bars — mirror web .bars */
|
||||
.bars__row { display: grid; grid-template-columns: 84px 1fr 28px; align-items: center; gap: 8px; margin-bottom: 5px; font-size: 11px; }
|
||||
.bars__key { color: #cbd5e1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.bars__track { background: #27272a; border-radius: 9999px; height: 8px; overflow: hidden; }
|
||||
.bars__fill { display: block; height: 8px; background: #10b981; border-radius: 9999px; }
|
||||
.bars__count { text-align: right; color: #94a3b8; font-family: ui-monospace, monospace; }
|
||||
/* funnel + highlights */
|
||||
.funnel { display: flex; gap: 8px; }
|
||||
.stage { flex: 1; background: #27272a; border-radius: 8px; padding: 10px 8px; text-align: center; }
|
||||
.stage__value { font-size: 20px; font-weight: 700; }
|
||||
.stage__value--accent { color: #34d399; }
|
||||
.stage__label { font-size: 9px; color: #94a3b8; margin-top: 2px; }
|
||||
.highlight { background: #27272a; border-radius: 8px; padding: 10px 14px; min-width: 150px; }
|
||||
.highlight__value { font-size: 18px; font-weight: 700; }
|
||||
.highlight__value--accent { color: #34d399; }
|
||||
.highlight__label { font-size: 9px; color: #94a3b8; margin-top: 2px; text-transform: uppercase; letter-spacing: .5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0a0a0c] flex items-center justify-center min-h-screen p-6 text-[#e2e8f0]">
|
||||
<div class="mac-window w-[1100px] bg-[#1c1c1f] border border-[#3f3f46] rounded-2xl overflow-hidden shadow-2xl flex flex-col" style="height: 760px;">
|
||||
<!-- macOS Title Bar -->
|
||||
<div class="h-9 bg-[#27272a] border-b border-[#3f3f46] flex items-center px-3 gap-x-2 flex-shrink-0">
|
||||
<div class="flex gap-x-1.5">
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#ff5f57] border border-[#e0443e]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#febc2e] border border-[#d9a023]"></div>
|
||||
<div class="w-[12px] h-[12px] rounded-full bg-[#28c840] border border-[#1a9e2b]"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<span class="text-sm font-medium text-primary">Quinn Prospector — Markets</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-2 text-[10px]">
|
||||
<div class="px-2 py-px bg-blue-900/60 text-blue-400 rounded text-[9px] font-mono flex items-center gap-x-1">
|
||||
<i class="fa-solid fa-clock fa-xs"></i>
|
||||
<span>tour-market = metro + date window + tz</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Market bar -->
|
||||
<div class="h-12 bg-[#18181b] border-b border-[#3f3f46] px-4 flex items-center justify-between flex-shrink-0">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<a href="index.html" class="text-muted hover:text-white text-xs"><i class="fa-solid fa-arrow-left"></i> Hub</a>
|
||||
<label class="flex items-center gap-x-2 text-xs">
|
||||
<span class="text-muted">Market</span>
|
||||
<select id="market-select" onchange="renderStats()" class="bg-[#27272a] border border-[#3f3f46] rounded-lg px-3 py-1.5 text-sm"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="seg" id="range-seg">
|
||||
<button class="seg__btn" data-days="7" onclick="setRange(7)">7d</button>
|
||||
<button class="seg__btn seg__btn--active" data-days="30" onclick="setRange(30)">30d</button>
|
||||
<button class="seg__btn" data-days="90" onclick="setRange(90)">90d</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto p-4 space-y-4" id="stats-body"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Mirror GET /prospector/markets + /market-stats
|
||||
const markets = [
|
||||
{ key: 'NYC', label: 'New York City', tz: 'America/New_York', prospects: 71, active: true },
|
||||
{ key: 'LA', label: 'Los Angeles', tz: 'America/Los_Angeles', prospects: 28, active: false },
|
||||
{ key: 'SF', label: 'San Francisco', tz: 'America/Los_Angeles', prospects: 15, active: false },
|
||||
];
|
||||
let currentDays = 30;
|
||||
|
||||
// Deterministic-ish per-market hour buckets (inbound + booked)
|
||||
function hoursFor(marketKey) {
|
||||
const base = marketKey === 'NYC' ? 1.0 : marketKey === 'LA' ? 0.5 : 0.3;
|
||||
const shape = [0,0,0,0,0,1,2,3,4,4,3,4, 5,4,3,4,6,8,11,13,12,9,6,3];
|
||||
return shape.map((v, hour) => {
|
||||
const inbound = Math.round(v * base);
|
||||
const booked = Math.round(inbound * (hour >= 18 && hour <= 22 ? 0.42 : 0.18));
|
||||
return { hour, inbound, booked };
|
||||
});
|
||||
}
|
||||
function conversionFor(hours) {
|
||||
return hours.map((h) => ({ hour: h.hour, total: h.inbound, booked: h.booked, rate: h.inbound ? h.booked / h.inbound : 0 }));
|
||||
}
|
||||
const dayShape = [
|
||||
{ weekday: 1, label: 'Mon', inbound: 14, booked: 3 },
|
||||
{ weekday: 2, label: 'Tue', inbound: 11, booked: 2 },
|
||||
{ weekday: 3, label: 'Wed', inbound: 16, booked: 4 },
|
||||
{ weekday: 4, label: 'Thu', inbound: 22, booked: 6 },
|
||||
{ weekday: 5, label: 'Fri', inbound: 28, booked: 9 },
|
||||
{ weekday: 6, label: 'Sat', inbound: 24, booked: 8 },
|
||||
{ weekday: 0, label: 'Sun', inbound: 12, booked: 3 },
|
||||
];
|
||||
|
||||
function statsFor(marketKey, days) {
|
||||
const m = markets.find((x) => x.key === marketKey);
|
||||
const scale = days / 30;
|
||||
const peakHours = hoursFor(marketKey).map((h) => ({ hour: h.hour, inbound: Math.round(h.inbound * scale * 4), booked: Math.round(h.booked * scale * 4) }));
|
||||
return {
|
||||
market: m,
|
||||
rangeDays: days,
|
||||
peakHours,
|
||||
conversionByHour: conversionFor(peakHours),
|
||||
peakDays: dayShape.map((d) => ({ ...d, inbound: Math.round(d.inbound * scale) })),
|
||||
funnel: { total: m.prospects, newInRange: Math.round(m.prospects * 0.45 * scale + 4), drafted: Math.round(m.prospects * 0.3 * scale + 3), sent: Math.round(m.prospects * 0.22 * scale + 2), qualified: Math.round(m.prospects * 0.18 * scale + 1) },
|
||||
bySegment: [{ key: 'dates', count: Math.round(m.prospects * 0.66) }, { key: 'digital', count: Math.round(m.prospects * 0.25) }, { key: 'life', count: Math.round(m.prospects * 0.09) }],
|
||||
byLocality: marketKey === 'NYC'
|
||||
? [{ key: 'Williamsburg', count: 24 }, { key: 'Manhattan', count: 19 }, { key: 'Brooklyn', count: 14 }, { key: 'Queens', count: 8 }]
|
||||
: [{ key: 'Downtown', count: 12 }, { key: 'Westside', count: 9 }, { key: 'Valley', count: 6 }],
|
||||
};
|
||||
}
|
||||
|
||||
const fmtHour = (h) => { const p = h < 12 ? 'am' : 'pm'; const h12 = h % 12 === 0 ? 12 : h % 12; return `${h12}${p}`; };
|
||||
const pct = (r) => `${Math.round(r * 100)}%`;
|
||||
|
||||
function setRange(d) {
|
||||
currentDays = d;
|
||||
document.querySelectorAll('#range-seg .seg__btn').forEach((b) => b.classList.toggle('seg__btn--active', +b.dataset.days === d));
|
||||
renderStats();
|
||||
}
|
||||
|
||||
function bars(rows) {
|
||||
const max = Math.max(1, ...rows.map((r) => r.count));
|
||||
return `<div class="bars">${rows.map((r) => `
|
||||
<div class="bars__row">
|
||||
<span class="bars__key">${r.key}</span>
|
||||
<span class="bars__track"><span class="bars__fill" style="width:${(r.count / max) * 100}%"></span></span>
|
||||
<span class="bars__count">${r.count}</span>
|
||||
</div>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const key = document.getElementById('market-select').value;
|
||||
const s = statsFor(key, currentDays);
|
||||
const maxIn = Math.max(1, ...s.peakHours.map((h) => h.inbound));
|
||||
const peakHour = s.peakHours.reduce((best, h) => (h.inbound > 0 && (!best || h.inbound > best.inbound) ? h : best), null);
|
||||
const best = s.conversionByHour.filter((b) => b.total >= 3).reduce((b1, b2) => (!b1 || b2.rate > b1.rate ? b2 : b1), null);
|
||||
const maxConv = Math.max(0.001, ...s.conversionByHour.map((b) => b.rate));
|
||||
|
||||
const hourCols = s.peakHours.map((h) => `
|
||||
<div class="vchart__col${peakHour && h.hour === peakHour.hour ? ' vchart__col--peak' : ''}" title="${fmtHour(h.hour)}: ${h.inbound} inbound · ${h.booked} booked">
|
||||
<div class="vchart__stack">
|
||||
<div class="vchart__seg vchart__seg--hold" style="height:${((h.inbound - h.booked) / maxIn) * 100}%"></div>
|
||||
<div class="vchart__seg vchart__seg--send" style="height:${(h.booked / maxIn) * 100}%"></div>
|
||||
</div>
|
||||
<div class="vchart__label">${h.hour % 6 === 0 ? h.hour : ''}</div>
|
||||
</div>`).join('');
|
||||
|
||||
const convCols = s.conversionByHour.map((b) => `
|
||||
<div class="vchart__col" title="${fmtHour(b.hour)}: ${pct(b.rate)} (${b.booked}/${b.total})">
|
||||
<div class="vchart__stack"><div class="vchart__seg vchart__seg--send" style="height:${(b.rate / maxConv) * 100}%"></div></div>
|
||||
<div class="vchart__label">${b.hour % 6 === 0 ? b.hour : ''}</div>
|
||||
</div>`).join('');
|
||||
|
||||
const f = s.funnel;
|
||||
document.getElementById('stats-body').innerHTML = `
|
||||
<section class="card p-4">
|
||||
<div class="card__title">${s.market.label} · ${s.market.tz} · last ${s.rangeDays}d</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="highlight">
|
||||
<div class="highlight__value">${peakHour ? fmtHour(peakHour.hour) : '—'}</div>
|
||||
<div class="highlight__label">Peak inbound hour</div>
|
||||
</div>
|
||||
<div class="highlight">
|
||||
<div class="highlight__value highlight__value--accent">${best ? `${fmtHour(best.hour)} · ${pct(best.rate)}` : '—'}</div>
|
||||
<div class="highlight__label">Best-converting hour</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card p-4">
|
||||
<div class="card__title">Peak hours · ${s.market.tz}
|
||||
<span class="ml-2 text-[10px] font-normal text-muted"><span class="inline-block w-2 h-2 rounded-sm align-middle" style="background:#3f3f46"></span> inbound · <span class="inline-block w-2 h-2 rounded-sm align-middle" style="background:#10b981"></span> booked</span>
|
||||
</div>
|
||||
<div class="vchart">${hourCols}</div>
|
||||
</section>
|
||||
|
||||
<section class="card p-4">
|
||||
<div class="card__title">Conversion by hour</div>
|
||||
<div class="vchart">${convCols}</div>
|
||||
</section>
|
||||
|
||||
<section class="card p-4">
|
||||
<div class="card__title">Peak days</div>
|
||||
${bars(s.peakDays.map((d) => ({ key: d.label, count: d.inbound })))}
|
||||
</section>
|
||||
|
||||
<section class="card p-4">
|
||||
<div class="card__title">Auto-qualify funnel</div>
|
||||
<div class="funnel">
|
||||
<div class="stage"><div class="stage__value">${f.total}</div><div class="stage__label">Prospects</div></div>
|
||||
<div class="stage"><div class="stage__value">${f.newInRange}</div><div class="stage__label">New ≤${s.rangeDays}d</div></div>
|
||||
<div class="stage"><div class="stage__value">${f.drafted}</div><div class="stage__label">Drafted</div></div>
|
||||
<div class="stage"><div class="stage__value">${f.sent}</div><div class="stage__label">Sent</div></div>
|
||||
<div class="stage"><div class="stage__value stage__value--accent">${f.qualified}</div><div class="stage__label">Qualified</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card p-4">
|
||||
<div class="card__title">By band & locality</div>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div>${bars(s.bySegment)}</div>
|
||||
<div>${bars(s.byLocality)}</div>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
const sel = document.getElementById('market-select');
|
||||
sel.innerHTML = markets.map((m) => `<option value="${m.key}">${m.label} · ${m.prospects} prospect${m.prospects === 1 ? '' : 's'}</option>`).join('');
|
||||
sel.value = (markets.find((m) => m.active) ?? markets[0]).key;
|
||||
renderStats();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue