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:
@@ -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:
|
||||
<span style={{ color: "#9ca3af", fontStyle: "italic" }}>„{optimizedQuery}"</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning banner */}
|
||||
<div
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user