feat: two-phase pre-screening with HTTP check + DeepSeek batch classification
Phase 1 (no AI credits): httpx checks every selected domain concurrently (30 parallel) with real browser UA — detects live/dead/parked/redirect. Parked: keyword scan in body/title + known parking host redirect check. Results saved to DB immediately; dead/parked never reach DeepSeek. Phase 2 (single DeepSeek call): all live-site titles + snippets bundled into ONE Replicate/DeepSeek-R1 request → returns niche + type for every domain in batch (up to 80 per call, parallelised if more). - app/prescreener.py (new): _check_one(), prescreen_domains(), classify_with_deepseek(), parking signal lists, same-domain redirect logic - app/db.py: prescreen_status/niche/site_type/prescreen_at columns + migrations; save_prescreen_results() upsert helper - app/main.py: POST /api/prescreen/batch endpoint - app/static/index.html: - 🔍 Pre-screen button (disabled while running, shows spinner) - Niche + Type columns in Browse and Leads tables (.pni/.pty pills) - Prescreen status colour dot (●) when niche not yet set - prescreening state flag; result toast shows per-status counts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,13 @@ input[type=range]{accent-color:var(--accent);width:100px;cursor:pointer}
|
||||
.bg{background:var(--surface2);color:var(--text);border:1px solid var(--border)}
|
||||
.bkd{background:#f59e0b22;color:var(--kd);border:1px solid #f59e0b44}
|
||||
.bai{background:#a855f722;color:#c084fc;border:1px solid #a855f744}
|
||||
.bps{background:#0f9d5822;color:#34d399;border:1px solid #0f9d5844}
|
||||
.sm{padding:4px 9px;font-size:11px}
|
||||
/* Niche / type pills */
|
||||
.pni{background:#0ea5e918;color:#38bdf8;border:1px solid #0ea5e933}
|
||||
.pty{background:#8b5cf618;color:#a78bfa;border:1px solid #8b5cf633}
|
||||
/* Prescreen status dot */
|
||||
.ps-live{color:#34d399} .ps-dead{color:#f87171} .ps-parked{color:#fbbf24} .ps-redirect{color:#94a3b8}
|
||||
|
||||
/* Table */
|
||||
.tw{overflow-x:auto;border-radius:var(--r);border:1px solid var(--border)}
|
||||
@@ -345,6 +351,10 @@ tr:hover td{background:rgba(255,255,255,.025)}
|
||||
<button class="btn bs" @click="enqueueSelected()" :disabled="selected.length===0">
|
||||
+ Enrich (<span x-text="selected.length"></span>)
|
||||
</button>
|
||||
<button class="btn bps" @click="prescreenSelected()" :disabled="selected.length===0||prescreening">
|
||||
<span x-show="!prescreening">🔍 Pre-screen (<span x-text="selected.length"></span>)</span>
|
||||
<span x-show="prescreening">⏳ Screening…</span>
|
||||
</button>
|
||||
<select x-model="aiLang" style="padding:4px 8px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);font-size:13px;cursor:pointer" title="Pitch language">
|
||||
<option value="ES">🇪🇸 ES</option>
|
||||
<option value="EN">🇬🇧 EN</option>
|
||||
@@ -363,6 +373,7 @@ tr:hover td{background:rgba(255,255,255,.025)}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th><th>Domain</th><th>Score</th><th>KD</th><th>AI</th>
|
||||
<th>Niche</th><th>Type</th>
|
||||
<th>Contact</th><th>CMS</th><th>SSL days</th>
|
||||
<th>Country</th><th>Live</th>
|
||||
</tr>
|
||||
@@ -396,6 +407,16 @@ tr:hover td{background:rgba(255,255,255,.025)}
|
||||
x-text="row.ai_lead_quality"></span>
|
||||
<span x-show="!row.ai_lead_quality" class="pill ai-none" style="cursor:pointer" @click="openModal(row)" title="Click to assess">—</span>
|
||||
</td>
|
||||
<!-- Niche -->
|
||||
<td>
|
||||
<span x-show="row.niche" class="pill pni" x-text="row.niche"></span>
|
||||
<span x-show="!row.niche" :class="prescreenStatusIcon(row.prescreen_status)" :title="row.prescreen_status||''" x-text="prescreenStatusIcon(row.prescreen_status)?'●':'—'"></span>
|
||||
</td>
|
||||
<!-- Type -->
|
||||
<td>
|
||||
<span x-show="row.site_type" class="pill pty" x-text="row.site_type"></span>
|
||||
<span x-show="!row.site_type" style="color:var(--border)">—</span>
|
||||
</td>
|
||||
<!-- Contact info -->
|
||||
<td>
|
||||
<div class="contact-chips" x-data="{c: parseContacts(row.contact_info)}">
|
||||
@@ -567,16 +588,16 @@ tr:hover td{background:rgba(255,255,255,.025)}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Quality</th><th>Domain</th><th>Score</th>
|
||||
<th>Quality</th><th>Domain</th><th>Score</th><th>Niche</th><th>Type</th>
|
||||
<th>Best Contact</th><th>All Contacts</th><th>Pitch</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="leadsLoading">
|
||||
<tr><td colspan="7" style="text-align:center;padding:24px;color:var(--muted)">Loading…</td></tr>
|
||||
<tr><td colspan="9" style="text-align:center;padding:24px;color:var(--muted)">Loading…</td></tr>
|
||||
</template>
|
||||
<template x-if="!leadsLoading && leadsData.length===0">
|
||||
<tr><td colspan="7" style="text-align:center;padding:24px;color:var(--muted)">No assessed leads yet — run 🤖 AI Assess on some domains in Browse</td></tr>
|
||||
<tr><td colspan="9" style="text-align:center;padding:24px;color:var(--muted)">No assessed leads yet — run 🤖 AI Assess on some domains in Browse</td></tr>
|
||||
</template>
|
||||
<template x-for="row in leadsData" :key="row.domain">
|
||||
<tr>
|
||||
@@ -587,6 +608,8 @@ tr:hover td{background:rgba(255,255,255,.025)}
|
||||
</td>
|
||||
<td><a :href="'http://'+row.domain" target="_blank" rel="noopener" x-text="row.domain"></a></td>
|
||||
<td><span class="score" :style="scoreBg(row.score)" x-text="row.score??'—'"></span></td>
|
||||
<td><span x-show="row.niche" class="pill pni" x-text="row.niche"></span><span x-show="!row.niche" style="color:var(--border)">—</span></td>
|
||||
<td><span x-show="row.site_type" class="pill pty" x-text="row.site_type"></span><span x-show="!row.site_type" style="color:var(--border)">—</span></td>
|
||||
<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px">
|
||||
<span :title="row.ai_contact_value" x-text="row.ai_contact_value||'—'"></span>
|
||||
</td>
|
||||
@@ -649,6 +672,7 @@ function app() {
|
||||
qst: {}, customDomains: '',
|
||||
leadsQ: {quality:'', country:'', limit:'50'},
|
||||
leadsData: [], leadsTotal: 0, leadsPage: 1, leadsLoading: false,
|
||||
prescreening: false,
|
||||
pipeline: {hot:{count:0,samples:[]},warm:{count:0,samples:[]},cold:{count:0,samples:[]}},
|
||||
toast: {show:false,msg:'',type:'success'},
|
||||
modal: {open:false, domain:'', ai:{}, sa:null},
|
||||
@@ -746,6 +770,39 @@ function app() {
|
||||
this.notify(`Queued ${d2.queued} Kit Digital domains for AI assessment [${this.aiLang}]`,'info');
|
||||
},
|
||||
|
||||
async prescreenSelected() {
|
||||
if(!this.selected.length || this.prescreening) return;
|
||||
this.prescreening = true;
|
||||
this.notify(`Pre-screening ${this.selected.length} domains… (may take ~30s)`, 'info');
|
||||
try {
|
||||
const r = await fetch('/api/prescreen/batch', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({domains: this.selected})
|
||||
});
|
||||
const d = await r.json();
|
||||
if(r.ok) {
|
||||
this.notify(
|
||||
`✅ ${d.live} live · 🅿 ${d.parked} parked · ↗ ${d.redirect} redirect · ☠ ${d.dead} dead · 🏷 ${d.classified} classified`,
|
||||
'success'
|
||||
);
|
||||
await this._fetch(); // refresh to show niche/type columns
|
||||
} else {
|
||||
this.notify('Error: ' + (d.error||'unknown'), 'error');
|
||||
}
|
||||
} catch(e) { this.notify('Pre-screen failed: '+e.message, 'error'); }
|
||||
this.prescreening = false;
|
||||
this.selected = [];
|
||||
},
|
||||
|
||||
prescreenStatusIcon(status) {
|
||||
if(status==='live') return 'ps-live';
|
||||
if(status==='dead') return 'ps-dead';
|
||||
if(status==='parked') return 'ps-parked';
|
||||
if(status==='redirect') return 'ps-redirect';
|
||||
return '';
|
||||
},
|
||||
|
||||
async loadLeads(reset=false) {
|
||||
if(reset) this.leadsPage = 1;
|
||||
this.leadsLoading = true;
|
||||
|
||||
Reference in New Issue
Block a user