This commit is contained in:
2026-06-26 19:34:37 +02:00
7 changed files with 96 additions and 49 deletions
+91 -44
View File
@@ -534,6 +534,19 @@ FB_SEARCHES = [
"who can design a website for my", "who can design a website for my",
"looking for someone to create a website", "looking for someone to create a website",
"recommendations for a web designer", "recommendations for a web designer",
"I need a website",
"I need a website built",
"need wordpress site",
"need a custom website",
"I need a new website",
"need an online store",
"need an online shop",
"need website help",
"who can help me with my website",
"looking for a website designer",
"need a shopify website",
"looking for website design",
"want to build a website for my",
] ]
VIEWPORTS = [ VIEWPORTS = [
@@ -1361,6 +1374,8 @@ async def ask_ollama(prompt: str) -> str:
async def classify_leads(results: list[dict]) -> list[dict]: async def classify_leads(results: list[dict]) -> list[dict]:
if not results: if not results:
return [] return []
# ── 1. AI classification ─────────────────────────────────────────
briefs = [r["title"][:200] for r in results] briefs = [r["title"][:200] for r in results]
prompt = f"""Classify each post as LEAD or NOT. prompt = f"""Classify each post as LEAD or NOT.
LEAD = someone REQUESTING/POSTING/WANTING a website built, designed, or created for them. LEAD = someone REQUESTING/POSTING/WANTING a website built, designed, or created for them.
@@ -1376,7 +1391,7 @@ NOT LEAD:
For each numbered post, answer ONLY "yes" (LEAD) or "no" (NOT LEAD): For each numbered post, answer ONLY "yes" (LEAD) or "no" (NOT LEAD):
{chr(10).join(f'{i+1}. {t}' for i, t in enumerate(briefs))} {chr(10).join(f'{i+1}. {t}' for i, t in enumerate(briefs))}
Return a JSON array like ["yes","no","yes"] matching the order above.""" Return a JSON array like ["yes","no","yes"] matching the order above."""
ai_succeeded = False ai_leads: list[dict] = []
try: try:
raw = await ask_ollama(prompt) raw = await ask_ollama(prompt)
raw = raw.strip() raw = raw.strip()
@@ -1389,55 +1404,87 @@ Return a JSON array like ["yes","no","yes"] matching the order above."""
raw = raw.strip() raw = raw.strip()
answers = json.loads(raw) answers = json.loads(raw)
if isinstance(answers, list) and len(answers) == len(results): if isinstance(answers, list) and len(answers) == len(results):
ai_succeeded = True
filtered = []
for i, ans in enumerate(answers): for i, ans in enumerate(answers):
if isinstance(ans, str) and ans.lower() == 'yes': if isinstance(ans, str) and ans.lower() == 'yes':
filtered.append(results[i]) ai_leads.append(results[i])
if filtered: logger.info("AI classified %d/%d as LEAD", len(ai_leads), len(results))
return filtered[:10]
logger.info("AI classified all %d items as NOT LEAD — returning empty", len(results))
return []
except Exception as e: except Exception as e:
logger.warning("AI classification failed, falling back to keyword filter: %s", e) logger.warning("AI classification failed: %s", e)
if not ai_succeeded: # ── 2. Keyword fallback (always runs) ────────────────────────────
web_terms = [ web_terms = [
"website", "web design", "web develop", "web dev", "website", "web design", "web develop", "web dev",
"web designer", "web developer", "web designer", "web developer",
"build my website", "build a website", "create a website", "build my website", "build a website", "create a website",
"landing page", "wordpress", "ecommerce", "landing page", "wordpress", "ecommerce",
"my website", "business website", "my website", "business website",
"site for my", "site for my business", "site for my", "site for my business",
"new website", "redesign my website", "new website", "redesign my website",
"help with my website", "update my website", "help with my website", "update my website",
] "make a website", "make my website",
request_terms = [ "website for my",
"looking for", "need a", "need an", "looking to", "online store", "online shop",
"need someone", "hire a", "want someone", "build my site", "build a site",
"need help with", "would like", "build me", "set up a website", "set up my website",
"design my", "make me a", "create my", "custom website",
"looking", "need", "want", "help", "shopify",
"who can", "i need", "my site",
"recommend", "anyone know", "anyone recommend", "webpage", "web page",
"know a", "know any", "recommendation", ]
"suggest", "looking for recommendations", request_terms = [
"can anyone", "does anyone", "looking for", "need a", "need an", "looking to",
] "need someone", "hire a", "want someone",
filtered = [] "need help with", "would like", "build me",
"design my", "make me a", "create my",
"looking", "need", "want", "help",
"who can", "i need",
"recommend", "anyone know", "anyone recommend",
"know a", "know any", "recommendation",
"suggest", "looking for recommendations",
"can anyone", "does anyone",
"dm me", "pm me", "message me",
"quote for",
"can you help",
"how much",
"price for",
]
keyword_leads: list[dict] = []
for r in results:
t = r['title'].lower()
has_web = any(kw in t for kw in web_terms)
has_request = any(kw in t for kw in request_terms)
if not has_web or not has_request:
continue
if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer',
'affordable web design package', 'web hosting',
'i do web design', 'i develop websites']):
continue
keyword_leads.append(r)
# ── 3. Merge: prefer AI leads, supplement with keywords to reach 5 ──
seen_titles: set[int] = set()
merged: list[dict] = []
for r in ai_leads + keyword_leads:
key = hash(r.get('title', ''))
if key not in seen_titles:
seen_titles.add(key)
merged.append(r)
# Fill to 5 with loose keyword matches (at least web OR request term)
if len(merged) < 5:
for r in results: for r in results:
key = hash(r.get('title', ''))
if key in seen_titles:
continue
t = r['title'].lower() t = r['title'].lower()
has_web = any(kw in t for kw in web_terms) if any(kw in t for kw in web_terms) or any(kw in t for kw in request_terms):
has_request = any(kw in t for kw in request_terms) seen_titles.add(key)
if not has_web or not has_request: merged.append(r)
continue if len(merged) >= 5:
if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer', break
'affordable web design package', 'web hosting',
'i do web design', 'i develop websites']): logger.info("classify_leads: %d merged (%d AI + %d keyword) from %d raw", len(merged), len(ai_leads), len(keyword_leads), len(results))
continue return merged[:10]
filtered.append(r)
return filtered[:10]
return []
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════
# FastAPI Endpoints # FastAPI Endpoints
+4 -4
View File
@@ -95,7 +95,7 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
"relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors", "relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
isActive isActive
? "bg-sidebar-primary text-sidebar-primary-foreground" ? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" : "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-white"
)} )}
> >
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
@@ -104,7 +104,7 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
)} )}
</Link> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" className="ml-2"> <TooltipContent side="right" className="ml-2 text-white">
{item.label} {item.label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -143,12 +143,12 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
onClick={() => setFbDialogOpen(true)} onClick={() => setFbDialogOpen(true)}
className="relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" className="relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-white"
> >
<Facebook className="h-5 w-5" /> <Facebook className="h-5 w-5" />
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" className="ml-2"> <TooltipContent side="right" className="ml-2 text-white">
Facebook Accounts Facebook Accounts
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
+1 -1
View File
@@ -36,7 +36,7 @@ export function SystemMonitor({ collapsed }: SystemMonitorProps) {
if (collapsed) { if (collapsed) {
return ( return (
<div className="border-t border-sidebar-border px-3 py-2 flex justify-center gap-1.5"> <div className="border-t border-sidebar-border px-1 py-1.5 flex flex-col items-center text-[10px] leading-tight">
<span className={ramOver ? "text-red-400" : "text-[var(--theme-primary,#3b91f7)]"}> <span className={ramOver ? "text-red-400" : "text-[var(--theme-primary,#3b91f7)]"}>
{rssMB} {rssMB}
</span> </span>