feat: GPT-4.1 optimierte Ergänzungssuche bei Maps-Lücke

Wenn Google Maps weniger Leads findet als angefragt, wird automatisch
eine optimierte Suchanfrage via GPT-4.1 generiert und als SERP-Job
gestartet, um die Lücke zu füllen. Die KI-Query wird im LoadingCard
angezeigt. Fallback auf Original-Query wenn kein OpenAI-Key konfiguriert.

- lib/services/openai.ts: GPT-4.1 Query-Generator
- app/api/search/supplement: neuer Endpoint (GPT + SERP-Job)
- LoadingCard: ruft /api/search/supplement statt direkt SERP
- apiKey.ts + .env.local.example: openai Key-Support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TimoUttenweiler
2026-04-01 10:43:33 +02:00
parent aa6707b8bc
commit 89a176700d
7 changed files with 156 additions and 20 deletions

View File

@@ -72,6 +72,7 @@ export function LoadingCard({ jobId, targetCount, query, region, onDone, onError
const [emailsFound, setEmailsFound] = useState(0);
const [progressWidth, setProgressWidth] = useState(3);
const [isTopping, setIsTopping] = useState(false);
const [optimizedQuery, setOptimizedQuery] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
@@ -95,35 +96,25 @@ export function LoadingCard({ jobId, targetCount, query, region, onDone, onError
});
}, 200);
async function startSerpSupplement(deficit: number) {
async function startSerpSupplement(foundCount: number) {
toppingActive = true;
setIsTopping(true);
setPhase("topping");
advanceBar(88);
const searchQuery = region ? `${query} ${region}` : query;
const maxPages = Math.max(1, Math.ceil(deficit / 10));
try {
const res = await fetch("/api/jobs/serp-enrich", {
const res = await fetch("/api/search/supplement", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: searchQuery,
maxPages,
countryCode: "de",
languageCode: "de",
filterSocial: true,
categories: ["ceo"],
enrichEmails: true,
}),
body: JSON.stringify({ query, region, targetCount, foundCount }),
});
if (!res.ok) throw new Error("SERP start failed");
const data = await res.json() as { jobId: string };
if (!res.ok) throw new Error("supplement start failed");
const data = await res.json() as { jobId: string; optimizedQuery: string; usedAI: boolean };
currentJobId = data.jobId;
if (data.optimizedQuery) setOptimizedQuery(data.optimizedQuery);
pollTimeout = setTimeout(poll, 2500);
} catch {
// SERP failed — complete with Maps results only
// Supplement failed — complete with Maps results only
if (!cancelled) {
setProgressWidth(100);
setPhase("done");
@@ -152,14 +143,14 @@ export function LoadingCard({ jobId, targetCount, query, region, onDone, onError
const leads = (data.results ?? []) as LeadResult[];
if (!toppingActive && data.totalLeads < targetCount) {
// Maps returned fewer than requested → supplement with SERP
// Maps returned fewer than requested → supplement with optimized SERP
mapsLeads = leads;
if (crawlInterval) clearInterval(crawlInterval);
crawlInterval = setInterval(() => {
if (cancelled) return;
setProgressWidth(prev => prev >= 96 ? prev : prev + 0.3);
}, 200);
await startSerpSupplement(targetCount - data.totalLeads);
await startSerpSupplement(data.totalLeads);
} else {
// All done
if (crawlInterval) clearInterval(crawlInterval);
@@ -303,6 +294,17 @@ export function LoadingCard({ jobId, targetCount, query, region, onDone, onError
)}
</div>
{/* Optimized query hint */}
{isTopping && optimizedQuery && (
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 14, fontSize: 11, color: "#6b7280" }}>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" strokeWidth="2.5">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636-.707.707M21 12h-1M4 12H3m3.343-5.657-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.374 3.374 0 0 0 14 18.469V19a2 2 0 1 1-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
KI-optimierte Suche:&nbsp;
<span style={{ color: "#9ca3af", fontStyle: "italic" }}>{optimizedQuery}"</span>
</div>
)}
{/* Warning banner */}
<div
style={{